├── .eslintrc.yml ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── BUILDING.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── FAQ.md ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── ROADMAP.md ├── assets ├── hmr_analyze.png ├── hmr_build.png ├── logo │ ├── hackmyresume-logo.ai │ └── hackmyresume-logo.png ├── office_space.jpg └── resume-bouqet.png ├── npm-shrinkwrap.json ├── package.json ├── src ├── cli │ ├── analyze.hbs │ ├── error.js │ ├── help │ │ ├── analyze.txt │ │ ├── build.txt │ │ ├── convert.txt │ │ ├── help.txt │ │ ├── new.txt │ │ ├── peek.txt │ │ ├── use.txt │ │ └── validate.txt │ ├── index.js │ ├── main.js │ ├── msg.js │ ├── msg.yml │ └── out.js ├── core │ ├── default-formats.js │ ├── default-options.js │ ├── empty-jrs.json │ ├── event-codes.js │ ├── fluent-date.js │ ├── fresh-resume.js │ ├── fresh-theme.js │ ├── jrs-resume.js │ ├── jrs-theme.js │ ├── resume-factory.js │ ├── resume.json │ └── status-codes.js ├── generators │ ├── base-generator.js │ ├── html-generator.js │ ├── html-pdf-cli-generator.js │ ├── html-png-generator.js │ ├── json-generator.js │ ├── json-yaml-generator.js │ ├── latex-generator.js │ ├── markdown-generator.js │ ├── template-generator.js │ ├── text-generator.js │ ├── word-generator.js │ ├── xml-generator.js │ └── yaml-generator.js ├── helpers │ ├── block-helpers.js │ ├── console-helpers.js │ ├── generic-helpers.js │ ├── handlebars-helpers.js │ └── underscore-helpers.js ├── index.js ├── inspectors │ ├── duration-inspector.js │ ├── gap-inspector.js │ ├── keyword-inspector.js │ └── totals-inspector.js ├── renderers │ ├── handlebars-generator.js │ ├── jrs-generator.js │ └── underscore-generator.js ├── utils │ ├── file-contains.js │ ├── fresh-version-regex.js │ ├── html-to-wpml.js │ ├── md2chalk.js │ ├── rasterize.js │ ├── resume-detector.js │ ├── resume-scrubber.js │ ├── safe-json-loader.js │ ├── safe-spawn.js │ ├── string-transformer.js │ ├── string.js │ └── syntax-error-ex.js └── verbs │ ├── analyze.js │ ├── build.js │ ├── convert.js │ ├── create.js │ ├── peek.js │ ├── validate.js │ └── verb.js └── test ├── .gitignore ├── all.js ├── expected └── modern │ ├── modern-html.css │ ├── modern-pdf.css │ ├── resume.doc │ ├── resume.html │ ├── resume.json │ ├── resume.md │ ├── resume.pdf │ ├── resume.pdf.html │ ├── resume.png │ ├── resume.png.html │ ├── resume.txt │ └── resume.yml └── scripts ├── hmr-options-broken.json ├── hmr-options.json ├── test-cli.js ├── test-dates.js ├── test-fresh-sheet.js ├── test-hmr.txt ├── test-jrs-sheet.js ├── test-output.js ├── test-themes.js └── test-verbs.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | es6: true 3 | node: true 4 | extends: 'eslint:recommended' 5 | rules: 6 | # indent: 7 | # - error 8 | # - 4 9 | linebreak-style: 10 | - error 11 | - unix 12 | quotes: 13 | - error 14 | - single 15 | semi: 16 | - error 17 | - always 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | 3 | * text=auto 4 | *.js text eol=lf 5 | *.json text eol=lf 6 | 7 | # Standard to msysgit 8 | 9 | *.doc diff=astextplain 10 | *.DOC diff=astextplain 11 | *.docx diff=astextplain 12 | *.DOCX diff=astextplain 13 | *.dot diff=astextplain 14 | *.DOT diff=astextplain 15 | *.pdf diff=astextplain 16 | *.PDF diff=astextplain 17 | *.rtf diff=astextplain 18 | *.RTF diff=astextplain 19 | 20 | # Git LFS 21 | 22 | *.ai filter=lfs diff=lfs merge=lfs -text 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | doc/ 3 | docs/ 4 | local/ 5 | npm-debug.log 6 | *.map 7 | 8 | # Emacs detritus 9 | # -*- mode: gitignore; -*- 10 | *~ 11 | \#*\# 12 | /.emacs.desktop 13 | /.emacs.desktop.lock 14 | *.elc 15 | auto-save-list 16 | tramp 17 | .\#* 18 | 19 | # Org-mode 20 | .org-id-locations 21 | *_archive 22 | 23 | # flymake-mode 24 | *_flymake.* 25 | 26 | # eshell files 27 | /eshell/history 28 | /eshell/lastdir 29 | 30 | # elpa packages 31 | /elpa/ 32 | 33 | # reftex files 34 | *.rel 35 | 36 | # AUCTeX auto folder 37 | /auto/ 38 | 39 | # cask packages 40 | .cask/ 41 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | assets/ 2 | test/ 3 | doc/ 4 | .travis.yml 5 | .eslintrc.yml 6 | Gruntfile.js 7 | .gitattributes 8 | ROADMAP.md 9 | BUILDING.md 10 | CONTRIBUTING.md 11 | CHANGELOG.md 12 | FAQ.md 13 | *.map 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | before_install: 3 | # Prevents a shared object .so error when running wkhtmltopdf on certain 4 | # platforms (e.g., vanilla Ubuntu 16.04 LTS). Not necessary on current Travis. 5 | # - sudo apt-get install libxrender1 6 | install: 7 | # Install & link HackMyResume 8 | - npm install && npm link 9 | # Download and extract the latest wkhtmltopdf binaries 10 | - mkdir tmp && wget https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.4/wkhtmltox-0.12.4_linux-generic-amd64.tar.xz -O tmp/wk.tar.xz 11 | - tar -xf tmp/wk.tar.xz -C ./tmp 12 | # Copy wkhtmltopdf binaries to /usr/bin (also makes them path-accessible) 13 | - sudo cp -R ./tmp/wkhtmltox/bin/* /usr/bin/ 14 | # Now you can invoke "wkhtmltopdf" and "wkhtmltoimage" safely in tests. 15 | - wkhtmltopdf -V 16 | - wkhtmltoimage -V 17 | language: node_js 18 | node_js: 19 | - "6" 20 | - "7" 21 | - "8" 22 | - "9" 23 | - "lts/*" 24 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | Building 2 | ======== 3 | 4 | *See [CONTRIBUTING.md][contrib] for more information on contributing to the 5 | HackMyResume or FluentCV projects.* 6 | 7 | HackMyResume is a standard Node.js command line app implemented in a mix of 8 | CoffeeScript and JavaScript. Setting up a build environment is easy: 9 | 10 | 11 | ## Prerequisites ## 12 | 13 | 1. OS: Linux, OS X, or Windows 14 | 15 | 2. Install [Node.js][node] and [Grunt][grunt]. 16 | 17 | 18 | ## Set up a build environment ### 19 | 20 | 1. Fork [hacksalot/HackMyResume][hmr] to your GitHub account. 21 | 22 | 2. Clone your fork locally. 23 | 24 | 3. From within the top-level HackMyResume folder, run `npm install` to install 25 | project dependencies. 26 | 27 | 4. Create a new branch, based on the latest HackMyResume `dev` branch, to 28 | contain your work. 29 | 30 | 5. Run `npm link` in the HackMyResume folder so that the `hackmyresume` command 31 | will reference your local installation (you may need to 32 | `npm uninstall -g hackmyresume` first). 33 | 34 | ## Making changes 35 | 36 | 1. HackMyResume sources live in the [`/src`][src] folder. 37 | 38 | 2. When you're ready to submit your changes, run `grunt test` to run the HMR 39 | test suite. Fix any errors that occur. 40 | 41 | 3. Commit and push your changes. 42 | 43 | 4. Submit a pull request targeting the HackMyResume `dev` branch. 44 | 45 | 46 | [node]: https://nodejs.org/en/ 47 | [grunt]: http://gruntjs.com/ 48 | [hmr]: https://github.com/hacksalot/HackMyResume 49 | [src]: https://github.com/hacksalot/HackMyResume/tree/master/src 50 | [contrib]: https://github.com/hacksalot/HackMyResume/blob/master/CONTRIBUTING.md 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | *Note: HackMyResume is also available as [FluentCV][fcv]. Contributors are 5 | credited in both.* 6 | 7 | ## How To Contribute 8 | 9 | *See [BUILDING.md][building] for instructions on setting up a HackMyResume 10 | development environment.* 11 | 12 | 1. Optional: [**open an issue**][iss] identifying the feature or bug you'd like 13 | to implement or fix. This step isn't required — you can start hacking away on 14 | HackMyResume without clearing it with us — but helps avoid duplication of work 15 | and ensures that your changes will be accepted once submitted. 16 | 2. **Fork and clone** the HackMyResume project. 17 | 3. Ideally, **create a new feature branch** (eg, `feat/new-awesome-feature` or 18 | similar; call it whatever you like) to perform your work in. 19 | 4. **Install dependencies** by running `npm install` in the top-level 20 | HackMyResume folder. 21 | 5. Make your **commits** as usual. 22 | 6. **Verify** your changes locally with `grunt test`. 23 | 7. **Push** your commits. 24 | 7. **Submit a pull request** from your feature branch to the HackMyResume `dev` 25 | branch. 26 | 8. We'll typically **respond** within 24 hours. 27 | 9. Your awesome changes will be **merged** after verification. 28 | 29 | ## Project Maintainers 30 | 31 | HackMyResume is currently maintained by [hacksalot][ha] with assistance from 32 | [tomheon][th] and our awesome [contributors][awesome]. Please direct all official 33 | or internal inquiries to: 34 | 35 | ``` 36 | admin@fluentdesk.com 37 | ``` 38 | 39 | You can reach hacksalot directly at: 40 | 41 | ``` 42 | hacksalot@indevious.com 43 | ``` 44 | 45 | Thanks for your interest in the HackMyResume project. 46 | 47 | [fcv]: https://github.com/fluentdesk/fluentcv 48 | [flow]: https://guides.github.com/introduction/flow/ 49 | [iss]: https://github.com/hacksalot/HackMyResume/issues 50 | [ha]: https://github.com/hacksalot 51 | [th]: https://github.com/tomheon 52 | [awesome]: https://github.com/hacksalot/HackMyResume/graphs/contributors 53 | [building]: https://github.com/hacksalot/HackMyResume/blob/master/BUILDING.md 54 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | Frequently Asked Questions (FAQ) 2 | ================================ 3 | 4 | ## How do I get started with HackMyResume? 5 | 6 | 1. Install with NPM: `[sudo] npm install hackmyresume -g`. 7 | 8 | 2. Create a new resume with: `hackmyresume NEW .json`. 9 | 10 | 3. Test with `hackmyresume BUILD .json`. Look in the `out/` folder. 11 | 12 | 4. Play around with different themes with the `-t` or `--theme` parameter. 13 | You can use any [FRESH](https://github.com/fluentdesk/fresh-themes) or 14 | [JSON Resume](https://jsonresume.org/themes) theme. The latter have to be 15 | installed first. 16 | 17 | ## What is FRESH? 18 | 19 | FRESH is the **F**luent **R**esume and **E**mployment **S**ystem for **H**umans. 20 | It's an open-source, user-first workflow, schema, and set of practices for 21 | technical candidates and recruiters. 22 | 23 | ## What is FRESCA? 24 | 25 | The **F**RESH **R**esume and **E**mployment **SC**hem**A**—an open-source, 26 | JSON-driven schema for resumes, CVs, and other employment artifacts. FRESCA is 27 | the recommended schema/format for FRESH, with optional support for JSON Resume. 28 | 29 | ## What is JSON Resume? 30 | 31 | An [open resume standard](http://jsonresume.org/themes/) sponsored by Hired.com. 32 | Like FRESCA, JSON Resume is JSON-driven and open-source. Unlike FRESCA, JSON 33 | Resume targets a worldwide audience where FRESCA is optimized for technical 34 | candidates. 35 | 36 | ## Should I use the FRESH or JSON Resume format/schema for my resume? 37 | 38 | Both! The workflow we like to use: 39 | 40 | 1. Create a resume in FRESH format for tooling and analysis. 41 | 2. Convert it to JSON Resume format for additional themes/tools. 42 | 3. Maintain both versions. 43 | 44 | Both formats are open-source and both formats are JSON-driven. FRESH was 45 | designed as a universal container format and superset of existing formats, where 46 | the JSON Resume format is intended for a generic audience. 47 | 48 | ## How do I use a FRESH theme? 49 | 50 | Several FRESH themes come preinstalled with HackMyResume; others can be 51 | installed from NPM and GitHub. 52 | 53 | ### To use a preinstalled FRESH theme: 54 | 55 | 1. Pass the theme name into HackMyResume via the `--theme` or `-t` parameter: 56 | 57 | ```bash 58 | hackmyresume build resume.json --theme compact 59 | ``` 60 | 61 | ### To use an external FRESH theme: 62 | 63 | 1. Install the theme locally. The easiest way to do that is with NPM. 64 | 65 | ```bash 66 | npm install fresh-theme-underscore 67 | ``` 68 | 69 | 2. Pass the theme folder into HackMyResume: 70 | 71 | ```bash 72 | hackmyresume BUILD resume.json --theme node_modules/fresh-theme-underscore 73 | ``` 74 | 75 | 3. Check your output folder. It's best to view HTML formats over a local web 76 | server connection. 77 | 78 | ## How do I use a JSON Resume theme? 79 | 80 | JSON Resume (JRS) themes can be installed from NPM and GitHub and passed into 81 | HackMyResume via the `--theme` or `-t` parameter. 82 | 83 | 1. Install the theme locally. The easiest way to do that is with NPM. 84 | 85 | ```bash 86 | npm install jsonresume-theme-classy 87 | ``` 88 | 89 | 2. Pass the theme folder path into HackMyResume: 90 | 91 | ```bash 92 | hackmyresume BUILD resume.json --theme node_modules/jsonresume-theme-classy 93 | ``` 94 | 95 | 3. Check your output folder. It's best to view HTML formats over a local web 96 | server connection. 97 | 98 | ## Should I keep my resume in version control? 99 | 100 | Absolutely! As text-based, JSON-driven documents, both FRESH and JSON Resume are 101 | ideal candidates for version control. Future versions of HackMyResume will have 102 | this functionality built in. 103 | 104 | ## Can I change the default section titles ("Employment", "Skills", etc.)? 105 | 106 | If you're using a FRESH theme, yes. First, create a HackMyResume options file 107 | mapping resume sections to your preferred section title: 108 | 109 | ```javascript 110 | // myoptions.json 111 | { 112 | "sectionTitles": { 113 | "employment": "empleo", 114 | "skills": "habilidades", 115 | "education": "educación" 116 | } 117 | } 118 | ``` 119 | 120 | Then, pass the options file into the `-o` or `--opts` parameter: 121 | 122 | ```bash 123 | hackmyresume BUILD resume.json -o myoptions.json 124 | ``` 125 | 126 | This ability is currently only supported for FRESH resume themes. 127 | 128 | ## How does resume merging work? 129 | 130 | Resume merging is a way of storing your resume in separate files that 131 | HackMyResume will merge into a single "master" resume file prior to generating 132 | specific output formats like HTML or PDF. It's a way of producing flexible, 133 | configurable, targeted resumes with minimal duplication. 134 | 135 | For example, a software developer who moonlights as a game programmer might 136 | create three FRESH or JRS resumes at different levels of specificity: 137 | 138 | - **generic.json**: A generic technical resume, suitable for all audiences. 139 | - **game-developer.json**: Overrides and amendments for game developer 140 | positions. 141 | - **blizzard.json**: Overrides and amendments specific to a hypothetical 142 | position at Blizzard. 143 | 144 | If you run `hackmyresume BUILD generic.json TO out/resume.all`, HMR will 145 | generate all available output formats for the `generic.json` as usual. But if 146 | you instead run... 147 | 148 | ```bash 149 | hackmyresume BUILD generic.json game-developer.json TO out/resume.all 150 | ``` 151 | 152 | ...HackMyResume will notice that multiple source resumes were specified and 153 | merge `game-developer.json` onto `generic.json` before generating, yielding a 154 | resume that's more suitable for game-developer-related positions. 155 | 156 | You can take this a step further. Let's say you want to do a targeted resume 157 | submission to a game developer position at Blizzard, and `blizzard.json` 158 | contains the edits and revisions you'd like to show up in the targeted resume. 159 | In that case, merge again! Feed all three resumes to HackMyResume, in order 160 | from most generic to most specific, and HMR will merge them all prior to 161 | generating the final output format(s) for your resume. 162 | 163 | ```bash 164 | # Merge blizzard.json onto game-developer.json onto generic.json, then build 165 | hackmyresume BUILD generic.json game-developer.json blizzard.json TO out/resume.all 166 | ``` 167 | 168 | There's no limit to the number of resumes you can merge this way. 169 | 170 | You can also divide your resume into files containing different sections: 171 | 172 | - **resume-a.json**: Contains `info`, `employment`, and `summary` sections. 173 | - **resume-b.json**: Contains all other sections except `references`. 174 | - **references.json**: Contains the private `references` section. 175 | 176 | Under that scenario, `hackmyresume BUILD resume-a.json resume-b.json` would 177 | 178 | 179 | ## The HackMyResume terminal color scheme is giving me a headache. Can I disable it? 180 | 181 | Yes. Use the `--no-color` option to disable terminal colors: 182 | 183 | `hackmyresume --no-color` 184 | 185 | ## What's the difference between a FRESH theme and a JSON Resume theme? 186 | 187 | FRESH themes are multiformat (HTML, Word, PDF, etc.) and required to support 188 | Markdown formatting, configurable section titles, and various other features. 189 | 190 | JSON Resume themes are typically HTML-driven, but capable of expansion to other 191 | formats through tools. JSON Resume themes don't support Markdown natively, but 192 | HMR does its best to apply your Markdown, when present, to any JSON Resume 193 | themes it encounters. 194 | 195 | ## Do I have to have a FRESH resume to use a FRESH theme or a JSON Resume to use a JSON Resume theme? 196 | 197 | No. You can mix and match FRESH and JRS-format themes freely. HackMyResume will 198 | perform the necessary conversions on the fly. 199 | 200 | ## Can I build my own custom FRESH theme? 201 | 202 | Yes. The easiest way is to copy an existing FRESH theme, like `modern` or 203 | `compact`, and make your changes there. You can test your theme with: 204 | 205 | ```bash 206 | hackmyresume build resume.json --theme path/to/my/theme/folder 207 | ``` 208 | 209 | ## Can I build my own custom JSON Resume theme? 210 | 211 | Yes. The easiest way is to copy an existing JSON Rsume theme and make your 212 | changes there. You can test your theme with: 213 | 214 | ```bash 215 | hackmyresume build resume.json --theme path/to/my/theme/folder 216 | ``` 217 | 218 | ## Can I build my own tools / services / apps / websites around FRESH / FRESCA? 219 | 220 | Yes! FRESH/FRESCA formats are 100% open source, permissively licensed under MIT, 221 | and 100% free from company-specific, tool-specific, or commercially oriented 222 | lock-in or cruft. These are clean formats designed for users and builders. 223 | 224 | ## Can I build my own tools / services / apps / websites around JSON Resume? 225 | 226 | Yes! HackMyResume is not affiliated with JSON Resume, but like FRESH/FRESCA, 227 | JSON Resume is open-source, permissively licensed, and free of proprietary 228 | lock-in. See the JSON Resume website for details. 229 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | 'use strict'; 4 | 5 | var opts = { 6 | 7 | pkg: grunt.file.readJSON('package.json'), 8 | 9 | simplemocha: { 10 | options: { 11 | globals: ['expect', 'should'], 12 | timeout: 3000, 13 | ignoreLeaks: false, 14 | ui: 'bdd', 15 | reporter: 'spec' 16 | }, 17 | all: { src: ['test/*.js'] } 18 | }, 19 | 20 | clean: { 21 | test: ['test/sandbox'] 22 | }, 23 | 24 | eslint: { 25 | target: ['Gruntfile.js', 'src/**/*.js', 'test/*.js'] 26 | } 27 | 28 | }; 29 | 30 | grunt.initConfig( opts ); 31 | grunt.loadNpmTasks('grunt-simple-mocha'); 32 | grunt.loadNpmTasks('grunt-eslint'); 33 | grunt.loadNpmTasks('grunt-contrib-clean'); 34 | 35 | // Use 'grunt test' for local testing 36 | grunt.registerTask('test', 'Test the HackMyResume application.', 37 | function() { 38 | grunt.task.run(['clean:test','build','eslint','simplemocha:all']); 39 | } 40 | ); 41 | 42 | // Use 'grunt build' to build HMR 43 | grunt.registerTask('build', 'Build the HackMyResume application.', 44 | function() { 45 | grunt.task.run( ['eslint'] ); 46 | } 47 | ); 48 | 49 | // Default task does everything 50 | grunt.registerTask('default', [ 'test' ]); 51 | 52 | }; 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright (c) 2015-2018 hacksalot (https://github.com/hacksalot) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | Development Roadmap 2 | =================== 3 | 4 | ## Short-Term 5 | 6 | ### FluentCV Desktop: Beta 1 7 | 8 | The **FluentCV Desktop 1.0 beta release** will present HackMyResume 9 | functionality in a cross-platform desktop application for OS X, Linux, and 10 | Windows. 11 | 12 | ### GitHub Integration 13 | 14 | HackMyResume will offer GitHub integration for versioned resume storage and 15 | retrieval via the `COMMIT` or `STORE` command(s) starting in 1.7.0 or 1.8.0. 16 | 17 | ### fresh-themes 1.0.0 18 | 19 | The **fresh-themes 1.0** release will bring 100% coverage of the FRESH and JRS 20 | object models—all resume sections and fields—along with 21 | documentation, theme developer's guide, new themes, and a freeze to the FRESH 22 | theme structure. 23 | 24 | ### Better LaTeX support 25 | 26 | Including Markdown-to-LaTeX translation and more LaTeX-driven themes / formats. 27 | 28 | ### StackOverflow and LinkedIn support 29 | 30 | Will start appearing in v1.7.0, with incremental improvements in 1.8.0 and 31 | beyond. 32 | 33 | ### Improved resume sorting and arranging 34 | 35 | **Better resume sorting** of items and sections: ascending, descending, by 36 | date or other criteria ([#67][i67]). 37 | 38 | ### Remote resume / theme loading 39 | 40 | Support remote loading of themes and resumes over `http`, `https`, and 41 | `git://`. Enable these usage patterns: 42 | 43 | ```bash 44 | hackmyresume build https://somesite.com/my-resume.json -t informatic 45 | 46 | hackmyresume build resume.json -t npm:fresh-theme-ergonomic 47 | 48 | hackmyresume analyze https://github.com/foo/my-resume 49 | ``` 50 | 51 | ### 100% code coverage 52 | 53 | Should reduce certain classes of errors and allow HMR to display a nifty 100% 54 | code coverage badge. 55 | 56 | ### Improved **documentation and samples** 57 | 58 | Expanded documentation and samples throughout. 59 | 60 | ## Mid-Term 61 | 62 | ### Cover letters and job descriptions 63 | 64 | Add support for schema-driven **cover letters** and **job descriptions**. 65 | 66 | ### Character Sheets 67 | 68 | HackMyResume 2.0 will ship with support for, yes, RPG-style character sheets. 69 | This will demonstrate the tool's ability to flow arbitrary JSON to concrete 70 | document(s) and provide unique albeit niche functionality around various games 71 | ([#117][i117]). 72 | 73 | ### Rich text (.rtf) output formats 74 | 75 | Basic support for **rich text** `.rtf` output formats. 76 | 77 | ### Investigate: groff support 78 | 79 | Investigate adding [**groff**][groff] support, because that would, indeed, be 80 | [dope][d] ([#37][i37]). 81 | 82 | ### Investigate: org-mode support 83 | 84 | Investigate adding [**org mode**][om] support ([#38][i38]). 85 | 86 | ### Investigate: Scribus 87 | 88 | Investigate adding [**Scribus SLA**][scri] support ([#54][i54]). 89 | 90 | ### Support JSON Resume 1.0.0 91 | 92 | When released. 93 | 94 | ## Long-Term 95 | 96 | - TBD 97 | 98 | [groff]: http://www.gnu.org/software/groff/ 99 | [om]: http://orgmode.org/ 100 | [scri]: https://en.wikipedia.org/wiki/Scribus 101 | [d]: https://github.com/hacksalot/HackMyResume/issues/37#issue-123818674 102 | [i37]: https://github.com/hacksalot/HackMyResume/issues/37 103 | [i38]: https://github.com/hacksalot/HackMyResume/issues/38 104 | [i54]: https://github.com/hacksalot/HackMyResume/issues/54 105 | [i67]: https://github.com/hacksalot/HackMyResume/issues/67 106 | [i107]: https://github.com/hacksalot/HackMyResume/issues/107 107 | [i117]: https://github.com/hacksalot/HackMyResume/issues/117 108 | -------------------------------------------------------------------------------- /assets/hmr_analyze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hacksalot/HackMyResume/ab6e7ee1a0f55608b531f4e644c298426291bb17/assets/hmr_analyze.png -------------------------------------------------------------------------------- /assets/hmr_build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hacksalot/HackMyResume/ab6e7ee1a0f55608b531f4e644c298426291bb17/assets/hmr_build.png -------------------------------------------------------------------------------- /assets/logo/hackmyresume-logo.ai: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a476ee59e7d86b5a7599780b5efca57ee6b6d60e1a722343277057ea793703b6 3 | size 1642116 4 | -------------------------------------------------------------------------------- /assets/logo/hackmyresume-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hacksalot/HackMyResume/ab6e7ee1a0f55608b531f4e644c298426291bb17/assets/logo/hackmyresume-logo.png -------------------------------------------------------------------------------- /assets/office_space.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hacksalot/HackMyResume/ab6e7ee1a0f55608b531f4e644c298426291bb17/assets/office_space.jpg -------------------------------------------------------------------------------- /assets/resume-bouqet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hacksalot/HackMyResume/ab6e7ee1a0f55608b531f4e644c298426291bb17/assets/resume-bouqet.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackmyresume", 3 | "version": "1.9.0-beta", 4 | "description": "Generate polished résumés and CVs in HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML, YAML, smoke signal, and carrier pigeon.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/hacksalot/HackMyResume.git" 8 | }, 9 | "scripts": { 10 | "test": "grunt clean:test && mocha --exit", 11 | "grunt": "grunt" 12 | }, 13 | "keywords": [ 14 | "resume", 15 | "CV", 16 | "portfolio", 17 | "employment", 18 | "career", 19 | "Markdown", 20 | "JSON", 21 | "Word", 22 | "PDF", 23 | "YAML", 24 | "HTML", 25 | "LaTeX", 26 | "CLI", 27 | "Handlebars", 28 | "Underscore", 29 | "template" 30 | ], 31 | "author": "hacksalot (https://github.com/hacksalot)", 32 | "contributors": [ 33 | "aruberto (https://github.com/aruberto)", 34 | "daniele-rapagnani (https://github.com/daniele-rapagnani)", 35 | "jjanusch (https://github.com/driftdev)", 36 | "robertmain (https://github.com/robertmain)", 37 | "tomheon (https://github.com/tomheon)", 38 | "zhuangya (https://github.com/zhuangya)", 39 | "hacksalot (https://github.com/hacksalot)" 40 | ], 41 | "license": "MIT", 42 | "preferGlobal": "true", 43 | "bugs": { 44 | "url": "https://github.com/hacksalot/HackMyResume/issues" 45 | }, 46 | "bin": { 47 | "hackmyresume": "src/cli/index.js" 48 | }, 49 | "main": "src/index.js", 50 | "homepage": "https://github.com/hacksalot/HackMyResume", 51 | "dependencies": { 52 | "chalk": "^2.3.1", 53 | "commander": "^2.9.0", 54 | "copy": "^0.3.1", 55 | "escape-latex": "^1.0.0", 56 | "extend": "^3.0.0", 57 | "fresh-jrs-converter": "^1.0.0", 58 | "fresh-resume-schema": "^1.0.0-beta", 59 | "fresh-resume-starter": "^0.3.1", 60 | "fresh-resume-validator": "^0.2.0", 61 | "fresh-themes": "^0.17.0-beta", 62 | "fs-extra": "^5.0.0", 63 | "glob": "^7.1.2", 64 | "handlebars": "^4.0.5", 65 | "html": "^1.0.0", 66 | "is-my-json-valid": "^2.12.4", 67 | "json-lint": "^0.1.0", 68 | "jsonlint": "^1.6.2", 69 | "lodash": "^4.17.5", 70 | "marked": "^0.3.5", 71 | "mkdirp": "^0.5.1", 72 | "moment": "^2.11.1", 73 | "parse-filepath": "^1.0.2", 74 | "path-exists": "^3.0.0", 75 | "pinkie-promise": "^2.0.0", 76 | "printf": "^0.2.3", 77 | "recursive-readdir-sync": "^1.0.6", 78 | "simple-html-tokenizer": "^0.4.3", 79 | "slash": "^1.0.0", 80 | "string-padding": "^1.0.2", 81 | "string.prototype.endswith": "^0.2.0", 82 | "string.prototype.startswith": "^0.2.0", 83 | "traverse": "^0.6.6", 84 | "underscore": "^1.8.3", 85 | "word-wrap": "^1.1.0", 86 | "xml-escape": "^1.0.0", 87 | "yamljs": "^0.3.0" 88 | }, 89 | "devDependencies": { 90 | "chai": "*", 91 | "chai-as-promised": "^7.1.1", 92 | "dir-compare": "^1.4.0", 93 | "fresh-test-resumes": "^0.9.2", 94 | "fresh-test-themes": "^0.2.0", 95 | "fresh-theme-underscore": "^0.1.1", 96 | "grunt": "*", 97 | "grunt-contrib-clean": "^1.1.0", 98 | "grunt-contrib-coffee": "^2.0.0", 99 | "grunt-contrib-copy": "^1.0.0", 100 | "grunt-eslint": "^20.1.0", 101 | "grunt-simple-mocha": "*", 102 | "jsonresume-theme-boilerplate": "^0.1.2", 103 | "jsonresume-theme-classy": "^1.0.9", 104 | "jsonresume-theme-modern": "0.0.18", 105 | "jsonresume-theme-sceptile": "^1.0.5", 106 | "mocha": "*", 107 | "stripcolorcodes": "^0.1.0" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/cli/analyze.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{style "SECTIONS (" "bold"}}{{style totals.numSections "white" }}{{style ")" "bold"}} 3 | 4 | employment: {{v totals.totals.employment "-" 2 "bold" }} 5 | projects: {{v totals.totals.projects "-" 2 "bold" }} 6 | education: {{v totals.totals.education "-" 2 "bold" }} 7 | service: {{v totals.totals.service "-" 2 "bold" }} 8 | skills: {{v totals.totals.skills "-" 2 "bold" }} 9 | writing: {{v totals.totals.writing "-" 2 "bold" }} 10 | speaking: {{v totals.totals.speaking "-" 2 "bold" }} 11 | reading: {{v totals.totals.reading "-" 2 "bold" }} 12 | social: {{v totals.totals.social "-" 2 "bold" }} 13 | references: {{v totals.totals.references "-" 2 "bold" }} 14 | testimonials: {{v totals.totals.testimonials "-" 2 "bold" }} 15 | languages: {{v totals.totals.languages "-" 2 "bold" }} 16 | interests: {{v totals.totals.interests "-" 2 "bold" }} 17 | 18 | {{style "COVERAGE (" "bold"}}{{style coverage.pct "white"}}{{style ")" "bold"}} 19 | 20 | Total Days: {{v coverage.duration.total "-" 5 "bold" }} 21 | Employed: {{v coverage.duration.work "-" 5 "bold" }} 22 | Gaps: {{v coverage.gaps.length "-" 5 "bold" }} [{{#if coverage.gaps.length }}{{#each coverage.gaps }}{{#unless @first}} {{/unless}}{{gapLength duration }}{{/each}}{{/if}}] 23 | Overlaps: {{v coverage.overlaps.length "-" 5 "bold" }} [{{#if coverage.overlaps.length }}{{#each coverage.overlaps }}{{#unless @first}} {{/unless}}{{gapLength duration }}{{/each}}{{/if}}] 24 | 25 | {{style "KEYWORDS (" "bold"}}{{style keywords.length "white" }}{{style ")" "bold"}} 26 | 27 | {{#each keywords }}{{{pad name 18}}}: {{v count "-" 5 "bold"}} mention{{#isPlural count}}s{{/isPlural}} 28 | {{/each}} 29 | ------------------------------- 30 | {{v keywords.length "0" 9 "bold"}} {{style "KEYWORDS" "bold"}} {{v keywords.totalKeywords "0" 5 "bold"}} {{style "mentions" "bold"}} 31 | -------------------------------------------------------------------------------- /src/cli/help/analyze.txt: -------------------------------------------------------------------------------- 1 | **analyze** | Analyze a resume for statistical insight 2 | 3 | Usage: 4 | 5 | **hackmyresume ANALYZE ** 6 | 7 | The ANALYZE command evaluates the specified resume(s) for 8 | coverage, duration, gaps, keywords, and other metrics. 9 | 10 | This command can be run against multiple resumes. Each 11 | will be analyzed in turn. 12 | 13 | Parameters: 14 | 15 | **** 16 | 17 | Path to a FRESH or JRS resume. Multiple resumes can be 18 | specified, separated by spaces. 19 | 20 | hackmyresume ANALYZE resume.json 21 | hackmyresume ANALYZE r1.json r2.json r3.json 22 | 23 | Options: 24 | 25 | **None.** 26 | -------------------------------------------------------------------------------- /src/cli/help/build.txt: -------------------------------------------------------------------------------- 1 | **build** | Generate themed resumes in multiple formats 2 | 3 | Usage: 4 | 5 | **hackmyresume BUILD TO [--theme]** 6 | **[--pdf] [--no-escape] [--private]** 7 | 8 | The BUILD command generates themed resumes and CVs in 9 | multiple formats. Use it to create outbound resumes in 10 | specific formats such HTML, MS Word, and PDF. 11 | 12 | Parameters: 13 | 14 | **** 15 | 16 | Path to a FRESH or JRS resume (*.json) containing your 17 | resume data. Multiple resumes may be specified. 18 | 19 | If multiple resumes are specified, they will be merged 20 | into a single resume prior to transformation. 21 | 22 | **** 23 | 24 | Path to the desired output resume. Multiple resumes 25 | may be specified. The file extension will determine 26 | the format. 27 | 28 | .all Generate all supported formats 29 | .html HTML 5 30 | .doc MS Word 31 | .pdf Adobe Acrobat PDF 32 | .txt plain text 33 | .md Markdown 34 | .png PNG Image 35 | .latex LaTeX 36 | 37 | Note: not all formats are supported by all themes! 38 | Check the theme's documentation for details or use 39 | the .all extension to build all available formats. 40 | 41 | Options: 42 | 43 | **--theme -t ** 44 | 45 | Path to a FRESH or JSON Resume theme OR the name of a 46 | built-in theme. Valid theme names are 'modern', 47 | 'positive', 'compact', 'awesome', and 'basis'. 48 | 49 | **--pdf -p ** 50 | 51 | Specify the PDF engine to use. Legal values are 52 | 'none', 'wkhtmltopdf', 'phantom', or 'weasyprint'. 53 | 54 | **--no-escape** 55 | 56 | Disable escaping / encoding of resume data during 57 | resume generation. Handlebars themes only. 58 | 59 | **--private** 60 | 61 | Include resume fields marked as private. 62 | 63 | Notes: 64 | 65 | The BUILD command can be run against multiple source as well 66 | as multiple target resumes. If multiple source resumes are 67 | provided, they will be merged into a single source resume 68 | before generation. If multiple output resumes are provided, 69 | each will be generated in turn. 70 | -------------------------------------------------------------------------------- /src/cli/help/convert.txt: -------------------------------------------------------------------------------- 1 | **convert** | Convert resumes between FRESH and JRS formats 2 | 3 | Usage: 4 | 5 | **hackmyresume CONVERT TO [--format]** 6 | 7 | The CONVERT command converts one or more resume documents 8 | between the FRESH Resume Schema and JSON Resume formats. 9 | 10 | Parameters: 11 | 12 | **** 13 | 14 | Path to a FRESH or JRS resume. Multiple resumes can be 15 | specified. 16 | 17 | **** 18 | 19 | The path of the converted resume. Multiple resumes can 20 | be specified, one per provided input resume. 21 | 22 | Options: 23 | 24 | **--format -f ** 25 | 26 | The desired format for the new resume(s). Valid values 27 | are 'FRESH', 'JRS', or, to target the latest edge 28 | version of the JSON Resume Schema, 'JRS@1'. 29 | 30 | If this parameter is omitted, the destination format 31 | will be inferred from the source resume's format. If 32 | the source format is FRESH, the destination format 33 | will be JSON Resume, and vice-versa. 34 | -------------------------------------------------------------------------------- /src/cli/help/help.txt: -------------------------------------------------------------------------------- 1 | **help** | View help on a specific HackMyResume command 2 | 3 | Usage: 4 | 5 | **hackmyresume HELP []** 6 | 7 | The HELP command displays help information for a specific 8 | HackMyResume command, including the HELP command itself. 9 | 10 | Parameters: 11 | 12 | **** 13 | 14 | The HackMyResume command to view help information for. 15 | Must be BUILD, NEW, CONVERT, ANALYZE, VALIDATE, PEEK, 16 | or HELP. 17 | 18 | hackmyresume help convert 19 | hackmyresume help help 20 | 21 | Options: 22 | 23 | **None.** 24 | -------------------------------------------------------------------------------- /src/cli/help/new.txt: -------------------------------------------------------------------------------- 1 | **new** | Create a new FRESH or JRS resume document 2 | 3 | Usage: 4 | 5 | **hackmyresume NEW [--format]** 6 | 7 | The NEW command generates a new resume document in FRESH 8 | or JSON Resume format. This document can serve as an 9 | official source of truth for your resume and career data 10 | as well an input to tools like HackMyResume. 11 | 12 | Parameters: 13 | 14 | **** 15 | 16 | The filename (relative or absolute path) of the resume 17 | to be created. Multiple resume paths can be specified, 18 | and each will be created in turn. 19 | 20 | hackmyresume NEW resume.json 21 | hackmyresume NEW r1.json foo/r2.json ../r3.json 22 | 23 | Options: 24 | 25 | **--format -f ** 26 | 27 | The desired format for the new resume(s). Valid values 28 | are 'FRESH', 'JRS', or, to target the latest edge 29 | version of the JSON Resume Schema, 'JRS@1'. 30 | -------------------------------------------------------------------------------- /src/cli/help/peek.txt: -------------------------------------------------------------------------------- 1 | **peek** | View portions of a resume from the command line 2 | 3 | Usage: 4 | 5 | **hackmyresume PEEK ** 6 | 7 | The PEEK command displays a specific piece or part of the 8 | resume without requiring the resume to be opened in an 9 | editor. 10 | 11 | Parameters: 12 | 13 | **** 14 | 15 | Path to a FRESH or JRS resume. Multiple resumes can be 16 | specified, separated by spaces. 17 | 18 | hackmyresume PEEK r1.json r2.json r3.json "employment.history[2]" 19 | 20 | **** 21 | 22 | The resume property or field to be displayed. Can be 23 | any valid resume path, for example: 24 | 25 | education[0] 26 | info.name 27 | employment.history[3].start 28 | 29 | Options: 30 | 31 | **None.** 32 | -------------------------------------------------------------------------------- /src/cli/help/use.txt: -------------------------------------------------------------------------------- 1 | **HackMyResume** | A Swiss Army knife for resumes and CVs 2 | 3 | Usage: 4 | 5 | **hackmyresume [--version] [--help] [--silent] [--debug]** 6 | **[--options] [--no-colors] []** 7 | 8 | Commands: (type "hackmyresume help COMMAND" for details) 9 | 10 | **BUILD** Build your resume to the destination format(s). 11 | **ANALYZE** Analyze your resume for keywords, gaps, and metrics. 12 | **VALIDATE** Validate your resume for errors and typos. 13 | **NEW** Create a new resume in FRESH or JSON Resume format. 14 | **CONVERT** Convert your resume between FRESH and JSON Resume. 15 | **PEEK** View a specific field or element on your resume. 16 | **HELP** View help on a specific HackMyResume command. 17 | 18 | Common Tasks: 19 | 20 | Generate a resume in a specific format (HTML, Word, PDF, etc.) 21 | 22 | **hackmyresume build rez.json to out/rez.html** 23 | **hackmyresume build rez.json to out/rez.doc** 24 | **hackmyresume build rez.json to out/rez.pdf** 25 | **hackmyresume build rez.json to out/rez.txt** 26 | **hackmyresume build rez.json to out/rez.md** 27 | **hackmyresume build rez.json to out/rez.png** 28 | **hackmyresume build rez.json to out/rez.tex** 29 | 30 | Build a resume to ALL available formats: 31 | 32 | **hackmyresume build rez.json to out/rez.all** 33 | 34 | Build a resume with a specific theme: 35 | 36 | **hackmyresume build rez.json to out/rez.all -t themeName** 37 | 38 | Create a new empty resume: 39 | 40 | **hackmyresume new rez.json** 41 | 42 | Convert a resume between FRESH and JRS formats: 43 | 44 | **hackmyresume convert rez.json converted.json** 45 | 46 | Analyze a resume for important metrics 47 | 48 | **hackmyresume analyze rez.json** 49 | 50 | Find more resume themes: 51 | 52 | **https://www.npmjs.com/search?q=jsonresume-theme** 53 | **https://www.npmjs.com/search?q=fresh-theme** 54 | **https://github.com/fresh-standard/fresh-themes** 55 | 56 | Validate a resume's structure and syntax: 57 | 58 | **hackmyresume validate resume.json** 59 | 60 | View help on a specific command: 61 | 62 | **hackmyresume help [build|convert|new|analyze|validate|peek|help]** 63 | 64 | Submit a bug or request: 65 | 66 | **https://githut.com/hacksalot/HackMyResume/issues** 67 | 68 | HackMyResume is free and open source software published 69 | under the MIT license. For more information, visit the 70 | HackMyResume website or GitHub project page. 71 | -------------------------------------------------------------------------------- /src/cli/help/validate.txt: -------------------------------------------------------------------------------- 1 | **validate** | Validate a resume for correctness 2 | 3 | Usage: 4 | 5 | **hackmyresume VALIDATE [--assert]** 6 | 7 | The VALIDATE command validates a FRESH or JRS document 8 | against its governing schema, verifying that the resume 9 | is correctly structured and formatted. 10 | 11 | Parameters: 12 | 13 | **** 14 | 15 | Path to a FRESH or JRS resume. Multiple resumes can be 16 | specified. 17 | 18 | hackmyresume ANALYZE resume.json 19 | hackmyresume ANALYZE r1.json r2.json r3.json 20 | 21 | Options: 22 | 23 | **--assert -a** 24 | 25 | Tell HackMyResume to return a non-zero process exit 26 | code if a resume fails to validate. 27 | -------------------------------------------------------------------------------- /src/cli/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | 4 | 5 | /** 6 | Command-line interface (CLI) for HackMyResume. 7 | @license MIT. See LICENSE.md for details. 8 | @module index.js 9 | */ 10 | 11 | 12 | 13 | try { 14 | 15 | require('./main')( process.argv ); 16 | 17 | } 18 | catch( ex ) { 19 | 20 | require('./error').err( ex, true ); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/cli/msg.js: -------------------------------------------------------------------------------- 1 | /** 2 | Message-handling routines for HackMyResume. 3 | @module cli/msg 4 | @license MIT. See LICENSE.md for details. 5 | */ 6 | 7 | 8 | const PATH = require('path'); 9 | const YAML = require('yamljs'); 10 | module.exports = YAML.load(PATH.join(__dirname, 'msg.yml')); 11 | -------------------------------------------------------------------------------- /src/cli/msg.yml: -------------------------------------------------------------------------------- 1 | events: 2 | begin: 3 | msg: Invoking **%s** command. 4 | beforeCreate: 5 | msg: Creating new **%s** resume: **%s** 6 | afterCreate: 7 | msg: Creating new **%s** resume: **%s** 8 | afterRead: 9 | msg: Reading **%s** resume: **%s** 10 | beforeTheme: 11 | msg: Verifying **%s** theme. 12 | afterTheme: 13 | msg: Verifying outputs: ??? 14 | beforeMerge: 15 | msg: 16 | - "Merging **%s**" 17 | - " onto **%s**" 18 | applyTheme: 19 | msg: Applying **%s** theme (**%s** format%s) 20 | afterBuild: 21 | msg: 22 | - "The **%s** theme says:" 23 | - | 24 | "For best results view JSON Resume themes over a 25 | local or remote HTTP connection. For example: 26 | 27 | npm install http-server -g 28 | http-server 29 | 30 | For more information, see the README." 31 | afterGenerate: 32 | msg: 33 | - " (with %s)" 34 | - "Skipping %s resume: %s" 35 | - "Generating **%s** resume: **%s**" 36 | beforeAnalyze: 37 | msg: "Analyzing **%s** resume: **%s**" 38 | beforeConvert: 39 | msg: "Converting **%s** (**%s**) to **%s** (**%s**)" 40 | afterValidate: 41 | msg: 42 | - "Validating **%s** against the **%s** schema: " 43 | - "VALID!" 44 | - "INVALID" 45 | - "BROKEN" 46 | - "MISSING" 47 | - "ERROR" 48 | beforePeek: 49 | msg: 50 | - Peeking at **%s** in **%s** 51 | - Peeking at **%s** 52 | afterPeek: 53 | msg: "The specified key **%s** was not found in **%s**." 54 | afterInlineConvert: 55 | msg: Converting **%s** to **%s** format. 56 | errors: 57 | themeNotFound: 58 | msg: > 59 | **Couldn't find the '%s' theme.** Please specify the name of a preinstalled 60 | FRESH theme or the path to a locally installed FRESH or JSON Resume theme. 61 | copyCSS: 62 | msg: Couldn't copy CSS file to destination folder. 63 | resumeNotFound: 64 | msg: Please **feed me a resume** in FRESH or JSON Resume format. 65 | missingCommand: 66 | msg: Please **give me a command** 67 | invalidCommand: 68 | msg: Invalid command: '%s' 69 | resumeNotFoundAlt: 70 | msg: Please **feed me a resume** in either FRESH or JSON Resume format. 71 | inputOutputParity: 72 | msg: Please **specify an output file name** for every input file you wish to convert. 73 | createNameMissing: 74 | msg: Please **specify the filename** of the resume to create. 75 | pdfGeneration: 76 | msg: PDF generation failed. Make sure wkhtmltopdf is installed and accessible from your path. 77 | invalid: 78 | msg: Validation failed and the --assert option was specified. 79 | invalidFormat: 80 | msg: The **%s** theme doesn't support the **%s** format. 81 | notOnPath: 82 | msg: %s wasn't found on your system path or is inaccessible. PDF not generated. 83 | readError: 84 | msg: Reading **???** resume: **%s** 85 | parseError: 86 | msg: 87 | - Invalid or corrupt JSON on line %s column %s. 88 | - Invalid or corrupt JSON on line %s. 89 | - Invalid or corrupt JSON. 90 | invalidHelperUse: 91 | msg: "**Warning**: Incorrect use of the **%s** theme helper." 92 | fileSaveError: 93 | msg: An error occurred while writing %s to disk: %s. 94 | mixedMerge: 95 | msg: "**Warning:** merging mixed resume types. Errors may occur." 96 | invokeTemplate: 97 | msg: "An error occurred during template invocation." 98 | compileTemplate: 99 | msg: "An error occurred during template compilation." 100 | themeLoad: 101 | msg: "Applying **%s** theme (? formats)" 102 | invalidParamCount: 103 | msg: "Invalid number of parameters. Expected: **%s**." 104 | missingParam: 105 | msg: The '**%s**' parameter was needed but not supplied. 106 | createError: 107 | msg: Failed to create **'%s'**. 108 | exiting: 109 | msg: Exiting with status code **%s**. 110 | validateError: 111 | msg: "An error occurred during validation:\n%s" 112 | invalidOptionsFile: 113 | msg: 114 | - "The specified options file is invalid:\n" 115 | - "\nMake sure the options file contains valid JSON." 116 | optionsFileNotFound: 117 | msg: "The specified options file is missing or inaccessible." 118 | unknownSchema: 119 | msg: 120 | - "Unknown resume schema. Did you specify a valid FRESH or JRS resume?" 121 | - | 122 | At a minimum, a FRESH resume must include a "name" field and a "meta" 123 | property. 124 | 125 | "name": "John Doe", 126 | "meta": { 127 | "format": "FRESH@0.1.0" 128 | } 129 | 130 | JRS-format resumes must include a "basics" section with a "name": 131 | 132 | "basics": { 133 | "name": "John Doe" 134 | } 135 | themeHelperLoad: 136 | msg: >- 137 | An error occurred while attempting to load the '%s' theme helper. Is the 138 | theme correctly installed? 139 | dummy: dontcare 140 | invalidSchemaVersion: 141 | msg: "'%s' is not recognized as a valid schema version." 142 | -------------------------------------------------------------------------------- /src/cli/out.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 5 | */ 6 | /** 7 | Output routines for HackMyResume. 8 | @license MIT. See LICENSE.md for details. 9 | @module cli/out 10 | */ 11 | 12 | 13 | 14 | const chalk = require('chalk'); 15 | const HME = require('../core/event-codes'); 16 | const _ = require('underscore'); 17 | const M2C = require('../utils/md2chalk.js'); 18 | const PATH = require('path'); 19 | const FS = require('fs'); 20 | const EXTEND = require('extend'); 21 | const HANDLEBARS = require('handlebars'); 22 | const YAML = require('yamljs'); 23 | let printf = require('printf'); 24 | const pad = require('string-padding'); 25 | const dbgStyle = 'cyan'; 26 | 27 | 28 | 29 | /** A stateful output module. All HMR console output handled here. */ 30 | class OutputHandler { 31 | 32 | 33 | 34 | constructor( opts ) { 35 | this.init(opts); 36 | } 37 | 38 | 39 | 40 | init(opts) { 41 | this.opts = EXTEND( true, this.opts || { }, opts ); 42 | this.msgs = YAML.load(PATH.join( __dirname, 'msg.yml' )).events; 43 | } 44 | 45 | 46 | 47 | log() { 48 | printf = require('printf'); 49 | const finished = printf.apply( printf, arguments ); 50 | return this.opts.silent || console.log( finished ); // eslint-disable-line no-console 51 | } 52 | 53 | 54 | 55 | do( evt ) { 56 | 57 | const that = this; 58 | const L = function() { return that.log.apply( that, arguments ); }; 59 | 60 | switch (evt.sub) { 61 | 62 | case HME.begin: 63 | return this.opts.debug && 64 | L( M2C( this.msgs.begin.msg, dbgStyle), evt.cmd.toUpperCase() ); 65 | 66 | //when HME.beforeCreate 67 | //L( M2C( this.msgs.beforeCreate.msg, 'green' ), evt.fmt, evt.file ) 68 | //break; 69 | 70 | case HME.afterCreate: 71 | L( M2C( this.msgs.beforeCreate.msg, evt.isError ? 'red' : 'green' ), evt.fmt, evt.file ); 72 | break; 73 | 74 | case HME.beforeTheme: 75 | return this.opts.debug && 76 | L( M2C( this.msgs.beforeTheme.msg, dbgStyle), evt.theme.toUpperCase() ); 77 | 78 | case HME.afterParse: 79 | return L( M2C( this.msgs.afterRead.msg, 'gray', 'white.dim'), evt.fmt.toUpperCase(), evt.file ); 80 | 81 | case HME.beforeMerge: 82 | var msg = ''; 83 | evt.f.reverse().forEach(function( a, idx ) { 84 | return msg += printf( (idx === 0 ? this.msgs.beforeMerge.msg[0] : this.msgs.beforeMerge.msg[1]), a.file ); 85 | } 86 | , this); 87 | return L( M2C(msg, (evt.mixed ? 'yellow' : 'gray'), 'white.dim') ); 88 | 89 | case HME.applyTheme: 90 | this.theme = evt.theme; 91 | var numFormats = Object.keys( evt.theme.formats ).length; 92 | return L( M2C(this.msgs.applyTheme.msg, 93 | evt.status === 'error' ? 'red' : 'gray', 94 | evt.status === 'error' ? 'bold' : 'white.dim'), 95 | evt.theme.name.toUpperCase(), 96 | numFormats, numFormats === 1 ? '' : 's' ); 97 | 98 | case HME.end: 99 | if (evt.cmd === 'build') { 100 | const themeName = this.theme.name.toUpperCase(); 101 | if (this.opts.tips && (this.theme.message || this.theme.render)) { 102 | if (this.theme.message) { 103 | L( M2C( this.msgs.afterBuild.msg[0], 'cyan' ), themeName ); 104 | return L( M2C( this.theme.message, 'white' )); 105 | } else if (this.theme.render) { 106 | L( M2C( this.msgs.afterBuild.msg[0], 'cyan'), themeName); 107 | return L( M2C( this.msgs.afterBuild.msg[1], 'white')); 108 | } 109 | } 110 | } 111 | break; 112 | 113 | case HME.afterGenerate: 114 | var suffix = ''; 115 | if (evt.fmt === 'pdf') { 116 | if (this.opts.pdf) { 117 | if (this.opts.pdf !== 'none') { 118 | suffix = printf( M2C( this.msgs.afterGenerate.msg[0], evt.error ? 'red' : 'green' ), this.opts.pdf ); 119 | } else { 120 | L( M2C( this.msgs.afterGenerate.msg[1], 'gray' ), evt.fmt.toUpperCase(), evt.file ); 121 | return; 122 | } 123 | } 124 | } 125 | 126 | return L( M2C( this.msgs.afterGenerate.msg[2] + suffix, evt.error ? 'red' : 'green' ), 127 | pad( evt.fmt.toUpperCase(),4,null,pad.RIGHT ), 128 | PATH.relative( process.cwd(), evt.file ) ); 129 | 130 | case HME.beforeAnalyze: 131 | return L( M2C( this.msgs.beforeAnalyze.msg, 'green' ), evt.fmt, evt.file); 132 | 133 | case HME.afterAnalyze: 134 | var { info } = evt; 135 | var rawTpl = FS.readFileSync( PATH.join( __dirname, 'analyze.hbs' ), 'utf8'); 136 | HANDLEBARS.registerHelper( require('../helpers/console-helpers') ); 137 | var template = HANDLEBARS.compile(rawTpl, { strict: false, assumeObjects: false }); 138 | var tot = 0; 139 | info.keywords.forEach(g => tot += g.count); 140 | info.keywords.totalKeywords = tot; 141 | var output = template( info ); 142 | return this.log( chalk.cyan(output) ); 143 | 144 | case HME.beforeConvert: 145 | return L( M2C( this.msgs.beforeConvert.msg, evt.error ? 'red' : 'green' ), 146 | evt.srcFile, evt.srcFmt, evt.dstFile, evt.dstFmt 147 | ); 148 | 149 | case HME.afterInlineConvert: 150 | return L( M2C( this.msgs.afterInlineConvert.msg, 'gray', 'white.dim' ), 151 | evt.file, evt.fmt ); 152 | 153 | case HME.afterValidate: 154 | var style = 'red'; 155 | var adj = ''; 156 | var msgs = this.msgs.afterValidate.msg; 157 | switch (evt.status) { 158 | case 'valid': style = 'green'; adj = msgs[1]; break; 159 | case 'invalid': style = 'yellow'; adj = msgs[2]; break; 160 | case 'broken': style = 'red'; adj = msgs[3]; break; 161 | case 'missing': style = 'red'; adj = msgs[4]; break; 162 | case 'unknown': style = 'red'; adj = msgs[5]; break; 163 | } 164 | evt.schema = evt.schema.replace('jars','JSON Resume').toUpperCase(); 165 | L(M2C( msgs[0], 'white' ) + chalk[style].bold(adj), evt.file, evt.schema); 166 | 167 | if (evt.violations) { 168 | _.each(evt.violations, function(err) { 169 | L( chalk.yellow.bold('--> ') + 170 | chalk.yellow(err.field.replace('data.','resume.').toUpperCase() + 171 | ' ' + err.message)); 172 | } 173 | , this); 174 | } 175 | return; 176 | 177 | case HME.afterPeek: 178 | var sty = evt.error ? 'red' : ( evt.target !== undefined ? 'green' : 'yellow' ); 179 | 180 | // "Peeking at 'someKey' in 'someFile'." 181 | if (evt.requested) { 182 | L(M2C(this.msgs.beforePeek.msg[0], sty), evt.requested, evt.file); 183 | } else { 184 | L(M2C(this.msgs.beforePeek.msg[1], sty), evt.file); 185 | } 186 | 187 | // If the key was present, print it 188 | if ((evt.target !== undefined) && !evt.error) { 189 | // eslint-disable-next-line no-console 190 | return console.dir( evt.target, { depth: null, colors: true } ); 191 | 192 | // If the key was not present, but no error occurred, print it 193 | } else if (!evt.error) { 194 | return L(M2C( this.msgs.afterPeek.msg, 'yellow'), evt.requested, evt.file); 195 | 196 | } else if (evt.error) { 197 | return L(chalk.red( evt.error.inner.inner )); 198 | } 199 | break; 200 | } 201 | } 202 | } 203 | 204 | module.exports = OutputHandler; 205 | -------------------------------------------------------------------------------- /src/core/default-formats.js: -------------------------------------------------------------------------------- 1 | /* 2 | Event code definitions. 3 | @module core/default-formats 4 | @license MIT. See LICENSE.md for details. 5 | */ 6 | 7 | /** Supported resume formats. */ 8 | module.exports = [ 9 | { name: 'html', ext: 'html', gen: new (require('../generators/html-generator'))() }, 10 | { name: 'txt', ext: 'txt', gen: new (require('../generators/text-generator'))() }, 11 | { name: 'doc', ext: 'doc', fmt: 'xml', gen: new (require('../generators/word-generator'))() }, 12 | { name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new (require('../generators/html-pdf-cli-generator'))() }, 13 | { name: 'png', ext: 'png', fmt: 'html', is: false, gen: new (require('../generators/html-png-generator'))() }, 14 | { name: 'md', ext: 'md', fmt: 'txt', gen: new (require('../generators/markdown-generator'))() }, 15 | { name: 'json', ext: 'json', gen: new (require('../generators/json-generator'))() }, 16 | { name: 'yml', ext: 'yml', fmt: 'yml', gen: new (require('../generators/json-yaml-generator'))() }, 17 | { name: 'latex', ext: 'tex', fmt: 'latex', gen: new (require('../generators/latex-generator'))() } 18 | ]; 19 | -------------------------------------------------------------------------------- /src/core/default-options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Event code definitions. 3 | @module core/default-options 4 | @license MIT. See LICENSE.md for details. 5 | */ 6 | 7 | module.exports = { 8 | theme: 'modern', 9 | prettify: { // ← See https://github.com/beautify-web/js-beautify#options 10 | indent_size: 2, 11 | unformatted: ['em','strong'], 12 | max_char: 80 13 | } // ← See lib/html.js in above-linked repo 14 | }; 15 | // wrap_line_length: 120, ← Don't use this 16 | -------------------------------------------------------------------------------- /src/core/empty-jrs.json: -------------------------------------------------------------------------------- 1 | { 2 | "basics": { 3 | "name": "", 4 | "label": "", 5 | "picture": "", 6 | "email": "", 7 | "phone": "", 8 | "degree": "", 9 | "website": "", 10 | "summary": "", 11 | "location": { 12 | "address": "", 13 | "postalCode": "", 14 | "city": "", 15 | "countryCode": "", 16 | "region": "" 17 | }, 18 | "profiles": [{ 19 | "network": "", 20 | "username": "", 21 | "url": "" 22 | }] 23 | }, 24 | 25 | "work": [{ 26 | "company": "", 27 | "position": "", 28 | "website": "", 29 | "startDate": "", 30 | "endDate": "", 31 | "summary": "", 32 | "highlights": [ 33 | "" 34 | ] 35 | }], 36 | 37 | "awards": [{ 38 | "title": "", 39 | "date": "", 40 | "awarder": "", 41 | "summary": "" 42 | }], 43 | 44 | "education": [{ 45 | "institution": "", 46 | "area": "", 47 | "studyType": "", 48 | "startDate": "", 49 | "endDate": "", 50 | "gpa": "", 51 | "courses": [ "" ] 52 | }], 53 | 54 | "publications": [{ 55 | "name": "", 56 | "publisher": "", 57 | "releaseDate": "", 58 | "website": "", 59 | "summary": "" 60 | }], 61 | 62 | "volunteer": [{ 63 | "organization": "", 64 | "position": "", 65 | "website": "", 66 | "startDate": "", 67 | "endDate": "", 68 | "summary": "", 69 | "highlights": [ "" ] 70 | }], 71 | 72 | "skills": [{ 73 | "name": "", 74 | "level": "", 75 | "keywords": [""] 76 | }] 77 | } 78 | -------------------------------------------------------------------------------- /src/core/event-codes.js: -------------------------------------------------------------------------------- 1 | /* 2 | Event code definitions. 3 | @module core/event-codes 4 | @license MIT. See LICENSE.md for details. 5 | */ 6 | 7 | 8 | module.exports = { 9 | error: -1, 10 | success: 0, 11 | begin: 1, 12 | end: 2, 13 | beforeRead: 3, 14 | afterRead: 4, 15 | beforeCreate: 5, 16 | afterCreate: 6, 17 | beforeTheme: 7, 18 | afterTheme: 8, 19 | beforeMerge: 9, 20 | afterMerge: 10, 21 | beforeGenerate: 11, 22 | afterGenerate: 12, 23 | beforeAnalyze: 13, 24 | afterAnalyze: 14, 25 | beforeConvert: 15, 26 | afterConvert: 16, 27 | verifyOutputs: 17, 28 | beforeParse: 18, 29 | afterParse: 19, 30 | beforePeek: 20, 31 | afterPeek: 21, 32 | beforeInlineConvert: 22, 33 | afterInlineConvert: 23, 34 | beforeValidate: 24, 35 | afterValidate: 25, 36 | beforeWrite: 26, 37 | afterWrite: 27, 38 | applyTheme: 28 39 | }; 40 | -------------------------------------------------------------------------------- /src/core/fluent-date.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * DS104: Avoid inline assignments 5 | * DS207: Consider shorter variations of null checks 6 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 7 | */ 8 | /** 9 | The HackMyResume date representation. 10 | @license MIT. See LICENSE.md for details. 11 | @module core/fluent-date 12 | */ 13 | 14 | 15 | 16 | const moment = require('moment'); 17 | require('../utils/string'); 18 | 19 | /** 20 | Create a FluentDate from a string or Moment date object. There are a few date 21 | formats to be aware of here. 22 | 1. The words "Present" and "Now", referring to the current date 23 | 2. The default "YYYY-MM-DD" format used in JSON Resume ("2015-02-10") 24 | 3. Year-and-month only ("2015-04") 25 | 4. Year-only "YYYY" ("2015") 26 | 5. The friendly HackMyResume "mmm YYYY" format ("Mar 2015" or "Dec 2008") 27 | 6. Empty dates ("", " ") 28 | 7. Any other date format that Moment.js can parse from 29 | Note: Moment can transparently parse all or most of these, without requiring us 30 | to specify a date format...but for maximum parsing safety and to avoid Moment 31 | deprecation warnings, it's recommended to either a) explicitly specify the date 32 | format or b) use an ISO format. For clarity, we handle these cases explicitly. 33 | @class FluentDate 34 | */ 35 | 36 | class FluentDate { 37 | 38 | constructor(dt) { 39 | this.rep = this.fmt(dt); 40 | } 41 | 42 | static isCurrent(dt) { 43 | return !dt || (String.is(dt) && /^(present|now|current)$/.test(dt)); 44 | } 45 | } 46 | 47 | const months = {}; 48 | const abbr = {}; 49 | moment.months().forEach((m,idx) => months[m.toLowerCase()] = idx+1); 50 | moment.monthsShort().forEach((m,idx) => abbr[m.toLowerCase()]=idx+1); 51 | abbr.sept = 9; 52 | module.exports = FluentDate; 53 | 54 | FluentDate.fmt = function( dt, throws ) { 55 | 56 | throws = ((throws === undefined) || (throws === null)) || throws; 57 | 58 | if ((typeof dt === 'string') || dt instanceof String) { 59 | dt = dt.toLowerCase().trim(); 60 | if (/^(present|now|current)$/.test(dt)) { // "Present", "Now" 61 | return moment(); 62 | } else if (/^\D+\s+\d{4}$/.test(dt)) { // "Mar 2015" 63 | let left; 64 | const parts = dt.split(' '); 65 | const month = (months[parts[0]] || abbr[parts[0]]); 66 | const temp = parts[1] + '-' + ((left = month < 10) != null ? left : `0${{month : month.toString()}}`); 67 | return moment(temp, 'YYYY-MM'); 68 | } else if (/^\d{4}-\d{1,2}$/.test(dt)) { // "2015-03", "1998-4" 69 | return moment(dt, 'YYYY-MM'); 70 | } else if (/^\s*\d{4}\s*$/.test(dt)) { // "2015" 71 | return moment(dt, 'YYYY'); 72 | } else if (/^\s*$/.test(dt)) { // "", " " 73 | return moment(); 74 | } else { 75 | const mt = moment(dt); 76 | if (mt.isValid()) { 77 | return mt; 78 | } 79 | if (throws) { 80 | throw 'Invalid date format encountered.'; 81 | } 82 | return null; 83 | } 84 | } else { 85 | if (!dt) { 86 | return moment(); 87 | } else if (dt.isValid && dt.isValid()) { 88 | return dt; 89 | } 90 | if (throws) { 91 | throw 'Unknown date object encountered.'; 92 | } 93 | return null; 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /src/core/fresh-theme.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * DS103: Rewrite code to no longer use __guard__ 5 | * DS207: Consider shorter variations of null checks 6 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 7 | */ 8 | /** 9 | Definition of the FRESHTheme class. 10 | @module core/fresh-theme 11 | @license MIT. See LICENSE.md for details. 12 | */ 13 | 14 | 15 | 16 | const FS = require('fs'); 17 | const _ = require('underscore'); 18 | const PATH = require('path'); 19 | const parsePath = require('parse-filepath'); 20 | const EXTEND = require('extend'); 21 | const HMSTATUS = require('./status-codes'); 22 | const loadSafeJson = require('../utils/safe-json-loader'); 23 | const READFILES = require('recursive-readdir-sync'); 24 | 25 | 26 | 27 | /* A representation of a FRESH theme asset. 28 | @class FRESHTheme */ 29 | class FRESHTheme { 30 | 31 | constructor() { 32 | this.baseFolder = 'src'; 33 | } 34 | 35 | /* Open and parse the specified theme. */ 36 | open( themeFolder ) { 37 | 38 | this.folder = themeFolder; 39 | 40 | // Set up a formats hash for the theme 41 | let formatsHash = { }; 42 | 43 | // Load the theme 44 | const themeFile = PATH.join(themeFolder, 'theme.json'); 45 | const themeInfo = loadSafeJson(themeFile); 46 | if (themeInfo.ex) { 47 | throw{ 48 | fluenterror: 49 | themeInfo.ex.op === 'parse' 50 | ? HMSTATUS.parseError 51 | : HMSTATUS.readError, 52 | inner: themeInfo.ex.inner 53 | }; 54 | } 55 | 56 | // Move properties from the theme JSON file to the theme object 57 | EXTEND(true, this, themeInfo.json); 58 | 59 | // Check for an "inherits" entry in the theme JSON. 60 | if (this.inherits) { 61 | const cached = { }; 62 | _.each(this.inherits, function(th, key) { 63 | // First, see if this is one of the predefined FRESH themes. There are 64 | // only a handful of these, but they may change over time, so we need to 65 | // query the official source of truth: the fresh-themes repository, which 66 | // mounts the themes conveniently by name to the module object, and which 67 | // is embedded locally inside the HackMyResume installation. 68 | // TODO: merge this code with 69 | let themePath; 70 | const themesObj = require('fresh-themes'); 71 | if (_.has(themesObj.themes, th)) { 72 | themePath = PATH.join( 73 | parsePath( require.resolve('fresh-themes') ).dirname, 74 | '/themes/', 75 | th 76 | ); 77 | } else { 78 | const d = parsePath( th ).dirname; 79 | themePath = PATH.join(d, th); 80 | } 81 | 82 | cached[ th ] = cached[th] || new FRESHTheme().open( themePath ); 83 | return formatsHash[ key ] = cached[ th ].getFormat( key ); 84 | }); 85 | } 86 | 87 | // Load theme files 88 | formatsHash = _load.call(this, formatsHash); 89 | 90 | // Cache 91 | this.formats = formatsHash; 92 | 93 | // Set the official theme name 94 | this.name = parsePath( this.folder ).name; 95 | return this; 96 | } 97 | 98 | /* Determine if the theme supports the specified output format. */ 99 | hasFormat( fmt ) { return _.has(this.formats, fmt); } 100 | 101 | /* Determine if the theme supports the specified output format. */ 102 | getFormat( fmt ) { return this.formats[ fmt ]; } 103 | } 104 | 105 | 106 | 107 | /* Load and parse theme source files. */ 108 | var _load = function(formatsHash) { 109 | 110 | const that = this; 111 | const tplFolder = PATH.join(this.folder, this.baseFolder); 112 | 113 | // Iterate over all files in the theme folder, producing an array, fmts, 114 | // containing info for each file. While we're doing that, also build up 115 | // the formatsHash object. 116 | const fmts = READFILES(tplFolder).map(function(absPath) { 117 | return _loadOne.call(this, absPath, formatsHash, tplFolder); 118 | } 119 | , this); 120 | 121 | // Now, get all the CSS files... 122 | this.cssFiles = fmts.filter(fmt => fmt && (fmt.ext === 'css')); 123 | 124 | // For each CSS file, get its corresponding HTML file. It's possible that 125 | // a theme can have a CSS file but *no* HTML file, as when a theme author 126 | // creates a pure CSS override of an existing theme. 127 | this.cssFiles.forEach(function(cssf) { 128 | const idx = _.findIndex(fmts, fmt => fmt && (fmt.pre === cssf.pre) && (fmt.ext === 'html')); 129 | cssf.major = false; 130 | if (idx > -1) { 131 | fmts[ idx ].css = cssf.data; 132 | return fmts[ idx ].cssPath = cssf.path; 133 | } else { 134 | if (that.inherits) { 135 | // Found a CSS file without an HTML file in a theme that inherits 136 | // from another theme. This is the override CSS file. 137 | return that.overrides = { file: cssf.path, data: cssf.data }; 138 | } 139 | }}); 140 | 141 | // Now, save all the javascript file paths to a theme property. 142 | const jsFiles = fmts.filter(fmt => fmt && (fmt.ext === 'js')); 143 | this.jsFiles = jsFiles.map(jsf => jsf['path']); 144 | 145 | return formatsHash; 146 | }; 147 | 148 | 149 | /* Load a single theme file. */ 150 | var _loadOne = function( absPath, formatsHash, tplFolder ) { 151 | 152 | const pathInfo = parsePath(absPath); 153 | if (pathInfo.basename.toLowerCase() === 'theme.json') { return; } 154 | 155 | const absPathSafe = absPath.trim().toLowerCase(); 156 | let outFmt = ''; 157 | let act = 'copy'; 158 | let isPrimary = false; 159 | 160 | // If this is an "explicit" theme, all files of importance are specified in 161 | // the "transform" section of the theme.json file. 162 | if (this.explicit) { 163 | 164 | outFmt = _.find(Object.keys( this.formats ), function( fmtKey ) { 165 | const fmtVal = this.formats[ fmtKey ]; 166 | return _.some(fmtVal.transform, function(fpath) { 167 | const absPathB = PATH.join( this.folder, fpath ).trim().toLowerCase(); 168 | return absPathB === absPathSafe; 169 | } 170 | , this); 171 | } 172 | , this); 173 | if (outFmt) { act = 'transform'; } 174 | } 175 | 176 | if (!outFmt) { 177 | // If this file lives in a specific format folder within the theme, 178 | // such as "/latex" or "/html", then that format is the implicit output 179 | // format for all files within the folder 180 | const portion = pathInfo.dirname.replace(tplFolder,''); 181 | if (portion && portion.trim()) { 182 | if (portion[1] === '_') { return; } 183 | const reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig; 184 | const res = reg.exec( portion ); 185 | if (res) { 186 | if (res[1] !== 'partials') { 187 | outFmt = res[1]; 188 | if (!this.explicit) { act = 'transform'; } 189 | } else { 190 | this.partials = this.partials || []; 191 | this.partials.push( { name: pathInfo.name, path: absPath } ); 192 | return null; 193 | } 194 | } 195 | } 196 | } 197 | 198 | // Otherwise, the output format is inferred from the filename, as in 199 | // compact-[outputformat].[extension], for ex, compact-pdf.html 200 | if (!outFmt) { 201 | const idx = pathInfo.name.lastIndexOf('-'); 202 | outFmt = idx === -1 ? pathInfo.name : pathInfo.name.substr(idx+1); 203 | if (!this.explicit) { act = 'transform'; } 204 | const defFormats = require('./default-formats'); 205 | isPrimary = _.some(defFormats, form => (form.name === outFmt) && (pathInfo.extname !== '.css')); 206 | } 207 | 208 | // Make sure we have a valid formatsHash 209 | formatsHash[ outFmt ] = formatsHash[outFmt] || { 210 | outFormat: outFmt, 211 | files: [] 212 | }; 213 | 214 | // Move symlink descriptions from theme.json to the format 215 | if (__guard__(this.formats != null ? this.formats[outFmt ] : undefined, x => x.symLinks)) { 216 | formatsHash[ outFmt ].symLinks = this.formats[ outFmt ].symLinks; 217 | } 218 | 219 | // Create the file representation object 220 | const obj = { 221 | action: act, 222 | primary: isPrimary, 223 | path: absPath, 224 | orgPath: PATH.relative(tplFolder, absPath), 225 | ext: pathInfo.extname.slice(1), 226 | title: friendlyName(outFmt), 227 | pre: outFmt, 228 | // outFormat: outFmt || pathInfo.name, 229 | data: FS.readFileSync(absPath, 'utf8'), 230 | css: null 231 | }; 232 | 233 | // Add this file to the list of files for this format type. 234 | formatsHash[ outFmt ].files.push( obj ); 235 | return obj; 236 | }; 237 | 238 | 239 | 240 | /* Return a more friendly name for certain formats. */ 241 | var friendlyName = function( val ) { 242 | val = (val && val.trim().toLowerCase()) || ''; 243 | const friendly = { yml: 'yaml', md: 'markdown', txt: 'text' }; 244 | return friendly[val] || val; 245 | }; 246 | 247 | 248 | 249 | module.exports = FRESHTheme; 250 | 251 | function __guard__(value, transform) { 252 | return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; 253 | } 254 | -------------------------------------------------------------------------------- /src/core/jrs-theme.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 5 | */ 6 | /** 7 | Definition of the JRSTheme class. 8 | @module core/jrs-theme 9 | @license MIT. See LICENSE.MD for details. 10 | */ 11 | 12 | 13 | 14 | const _ = require('underscore'); 15 | const PATH = require('path'); 16 | const pathExists = require('path-exists').sync; 17 | const errors = require('./status-codes'); 18 | 19 | 20 | 21 | /** 22 | The JRSTheme class is a representation of a JSON Resume theme asset. 23 | @class JRSTheme 24 | */ 25 | class JRSTheme { 26 | 27 | 28 | 29 | /** 30 | Open and parse the specified JRS theme. 31 | @method open 32 | */ 33 | open( thFolder ) { 34 | 35 | this.folder = thFolder; 36 | //const pathInfo = parsePath(thFolder); 37 | 38 | // Open and parse the theme's package.json file 39 | const pkgJsonPath = PATH.join(thFolder, 'package.json'); 40 | if (pathExists(pkgJsonPath)) { 41 | const thApi = require(thFolder); // Requiring the folder yields whatever the package.json's "main" is set to 42 | const thPkg = require(pkgJsonPath); // Get the package.json as JSON 43 | this.name = thPkg.name; 44 | this.render = (thApi && thApi.render) || undefined; 45 | this.engine = 'jrs'; 46 | 47 | // Create theme formats (HTML and PDF). Just add the bare minimum mix of 48 | // properties necessary to allow JSON Resume themes to share a rendering 49 | // path with FRESH themes. 50 | this.formats = { 51 | html: { 52 | outFormat: 'html', 53 | files: [{ 54 | action: 'transform', 55 | render: this.render, 56 | primary: true, 57 | ext: 'html', 58 | css: null 59 | }] 60 | }, 61 | pdf: { 62 | outFormat: 'pdf', 63 | files: [{ 64 | action: 'transform', 65 | render: this.render, 66 | primary: true, 67 | ext: 'pdf', 68 | css: null 69 | }] 70 | } 71 | }; 72 | } else { 73 | throw {fluenterror: errors.missingPackageJSON}; 74 | } 75 | return this; 76 | } 77 | 78 | 79 | 80 | /** 81 | Determine if the theme supports the output format. 82 | @method hasFormat 83 | */ 84 | hasFormat( fmt ) { return _.has(this.formats, fmt); } 85 | 86 | 87 | 88 | /** 89 | Return the requested output format. 90 | @method getFormat 91 | */ 92 | getFormat( fmt ) { return this.formats[ fmt ]; } 93 | } 94 | 95 | 96 | module.exports = JRSTheme; 97 | -------------------------------------------------------------------------------- /src/core/resume-factory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 5 | */ 6 | /** 7 | Definition of the ResumeFactory class. 8 | @license MIT. See LICENSE.md for details. 9 | @module core/resume-factory 10 | */ 11 | 12 | 13 | 14 | const FS = require('fs'); 15 | const HMS = require('./status-codes'); 16 | const HME = require('./event-codes'); 17 | const ResumeConverter = require('fresh-jrs-converter'); 18 | const resumeDetect = require('../utils/resume-detector'); 19 | require('string.prototype.startswith'); 20 | 21 | 22 | 23 | /** 24 | A simple factory class for FRESH and JSON Resumes. 25 | @class ResumeFactory 26 | */ 27 | 28 | module.exports = { 29 | 30 | 31 | 32 | /** 33 | Load one or more resumes from disk. 34 | 35 | @param {Object} opts An options object with settings for the factory as well 36 | as passthrough settings for FRESHResume or JRSResume. Structure: 37 | 38 | { 39 | format: 'FRESH', // Format to open as. ('FRESH', 'JRS', null) 40 | objectify: true, // FRESH/JRSResume or raw JSON? 41 | inner: { // Passthru options for FRESH/JRSResume 42 | sort: false 43 | } 44 | } 45 | 46 | */ 47 | load( sources, opts, emitter ) { 48 | return sources.map( function(src) { 49 | return this.loadOne( src, opts, emitter ); 50 | } 51 | , this); 52 | }, 53 | 54 | 55 | /** Load a single resume from disk. */ 56 | loadOne( src, opts, emitter ) { 57 | 58 | let toFormat = opts.format; // Can be null 59 | 60 | // Get the destination format. Can be 'fresh', 'jrs', or null/undefined. 61 | toFormat && (toFormat = toFormat.toLowerCase().trim()); 62 | 63 | // Load and parse the resume JSON 64 | const info = _parse(src, opts, emitter); 65 | if (info.fluenterror) { return info; } 66 | 67 | // Determine the resume format: FRESH or JRS 68 | let { json } = info; 69 | const orgFormat = resumeDetect(json); 70 | if (orgFormat === 'unk') { 71 | info.fluenterror = HMS.unknownSchema; 72 | return info; 73 | } 74 | 75 | // Convert between formats if necessary 76 | if (toFormat && ( orgFormat !== toFormat )) { 77 | json = ResumeConverter[ `to${toFormat.toUpperCase()}` ](json); 78 | } 79 | 80 | // Objectify the resume, that is, convert it from JSON to a FRESHResume 81 | // or JRSResume object. 82 | let rez = null; 83 | if (opts.objectify) { 84 | const reqLib = `../core/${toFormat || orgFormat}-resume`; 85 | const ResumeClass = require(reqLib); 86 | rez = new ResumeClass().parseJSON( json, opts.inner ); 87 | rez.i().file = src; 88 | } 89 | 90 | return { 91 | file: src, 92 | json: info.json, 93 | rez 94 | }; 95 | } 96 | }; 97 | 98 | 99 | var _parse = function( fileName, opts, eve ) { 100 | 101 | let rawData = null; 102 | try { 103 | 104 | // Read the file 105 | eve && eve.stat( HME.beforeRead, { file: fileName }); 106 | rawData = FS.readFileSync( fileName, 'utf8' ); 107 | eve && eve.stat( HME.afterRead, { file: fileName, data: rawData }); 108 | 109 | // Parse the file 110 | eve && eve.stat(HME.beforeParse, { data: rawData }); 111 | const ret = { json: JSON.parse( rawData ) }; 112 | const orgFormat = 113 | ret.json.meta && ret.json.meta.format && ret.json.meta.format.startsWith('FRESH@') 114 | ? 'fresh' : 'jrs'; 115 | 116 | eve && eve.stat(HME.afterParse, { file: fileName, data: ret.json, fmt: orgFormat }); 117 | return ret; 118 | } catch (err) { 119 | // Can be ENOENT, EACCES, SyntaxError, etc. 120 | return { 121 | fluenterror: rawData ? HMS.parseError : HMS.readError, 122 | inner: err, 123 | raw: rawData, 124 | file: fileName 125 | }; 126 | } 127 | }; 128 | -------------------------------------------------------------------------------- /src/core/status-codes.js: -------------------------------------------------------------------------------- 1 | /** 2 | Status codes for HackMyResume. 3 | @module core/status-codes 4 | @license MIT. See LICENSE.MD for details. 5 | */ 6 | 7 | 8 | module.exports = { 9 | success: 0, 10 | themeNotFound: 1, 11 | copyCss: 2, 12 | resumeNotFound: 3, 13 | missingCommand: 4, 14 | invalidCommand: 5, 15 | resumeNotFoundAlt: 6, 16 | inputOutputParity: 7, 17 | createNameMissing: 8, 18 | pdfGeneration: 9, 19 | missingPackageJSON: 10, 20 | invalid: 11, 21 | invalidFormat: 12, 22 | notOnPath: 13, 23 | readError: 14, 24 | parseError: 15, 25 | fileSaveError: 16, 26 | generateError: 17, 27 | invalidHelperUse: 18, 28 | mixedMerge: 19, 29 | invokeTemplate: 20, 30 | compileTemplate: 21, 31 | themeLoad: 22, 32 | invalidParamCount: 23, 33 | missingParam: 24, 34 | createError: 25, 35 | validateError: 26, 36 | invalidOptionsFile: 27, 37 | optionsFileNotFound: 28, 38 | unknownSchema: 29, 39 | themeHelperLoad: 30, 40 | invalidSchemaVersion: 31 41 | }; 42 | -------------------------------------------------------------------------------- /src/generators/base-generator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS206: Consider reworking classes to avoid initClass 4 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 5 | */ 6 | /** 7 | Definition of the BaseGenerator class. 8 | @module generators/base-generator 9 | @license MIT. See LICENSE.md for details. 10 | */ 11 | 12 | 13 | /** 14 | The BaseGenerator class is the root of the generator hierarchy. Functionality 15 | common to ALL generators lives here. 16 | */ 17 | 18 | let BaseGenerator; 19 | module.exports = (BaseGenerator = (function() { 20 | BaseGenerator = class BaseGenerator { 21 | static initClass() { 22 | 23 | /** Status codes. */ 24 | this.prototype.codes = require('../core/status-codes'); 25 | 26 | /** Generator options. */ 27 | this.prototype.opts = { }; 28 | } 29 | 30 | /** Base-class initialize. */ 31 | constructor( format ) { 32 | this.format = format; 33 | } 34 | }; 35 | BaseGenerator.initClass(); 36 | return BaseGenerator; 37 | })()); 38 | -------------------------------------------------------------------------------- /src/generators/html-generator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 5 | */ 6 | /** 7 | Definition of the HTMLGenerator class. 8 | @module generators/html-generator 9 | @license MIT. See LICENSE.md for details. 10 | */ 11 | 12 | 13 | 14 | const TemplateGenerator = require('./template-generator'); 15 | const HTML = require('html'); 16 | require('string.prototype.endswith'); 17 | 18 | 19 | 20 | class HtmlGenerator extends TemplateGenerator { 21 | 22 | constructor() { super('html'); } 23 | 24 | /** 25 | Copy satellite CSS files to the destination and optionally pretty-print 26 | the HTML resume prior to saving. 27 | */ 28 | onBeforeSave( info ) { 29 | if (info.outputFile.endsWith('.css')) { 30 | return info.mk; 31 | } 32 | if (this.opts.prettify) { 33 | return HTML.prettyPrint(info.mk, this.opts.prettify); 34 | } else { return info.mk; } 35 | } 36 | } 37 | 38 | 39 | module.exports = HtmlGenerator; 40 | -------------------------------------------------------------------------------- /src/generators/html-pdf-cli-generator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS103: Rewrite code to no longer use __guard__ 4 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 5 | */ 6 | /** 7 | Definition of the HtmlPdfCLIGenerator class. 8 | @module generators/html-pdf-generator.js 9 | @license MIT. See LICENSE.md for details. 10 | */ 11 | 12 | 13 | 14 | const TemplateGenerator = require('./template-generator'); 15 | const FS = require('fs-extra'); 16 | const PATH = require('path'); 17 | const SLASH = require('slash'); 18 | const _ = require('underscore'); 19 | const HMSTATUS = require('../core/status-codes'); 20 | const SPAWN = require('../utils/safe-spawn'); 21 | 22 | 23 | /** 24 | An HTML-driven PDF resume generator for HackMyResume. Talks to Phantom, 25 | wkhtmltopdf, and other PDF engines over a CLI (command-line interface). 26 | If an engine isn't installed for a particular platform, error out gracefully. 27 | */ 28 | 29 | class HtmlPdfCLIGenerator extends TemplateGenerator { 30 | 31 | 32 | 33 | constructor() { super('pdf', 'html'); } 34 | 35 | 36 | 37 | /** Generate the binary PDF. */ 38 | onBeforeSave( info ) { 39 | //console.dir _.omit( info, 'mk' ), depth: null, colors: true 40 | if ((info.ext !== 'html') && (info.ext !== 'pdf')) { return info.mk; } 41 | let safe_eng = info.opts.pdf || 'wkhtmltopdf'; 42 | if (safe_eng === 'phantom') { safe_eng = 'phantomjs'; } 43 | if (_.has(engines, safe_eng)) { 44 | this.errHandler = info.opts.errHandler; 45 | engines[ safe_eng ].call(this, info.mk, info.outputFile, info.opts, this.onError); 46 | return null; // halt further processing 47 | } 48 | } 49 | 50 | 51 | 52 | /* Low-level error callback for spawn(). May be called after HMR process 53 | termination, so object references may not be valid here. That's okay; if 54 | the references are invalid, the error was already logged. We could use 55 | spawn-watch here but that causes issues on legacy Node.js. */ 56 | onError(ex, param) { 57 | __guardMethod__(param.errHandler, 'err', o => o.err(HMSTATUS.pdfGeneration, ex)); 58 | } 59 | } 60 | 61 | module.exports = HtmlPdfCLIGenerator; 62 | 63 | // TODO: Move each engine to a separate module 64 | var engines = { 65 | 66 | 67 | 68 | /** 69 | Generate a PDF from HTML using wkhtmltopdf's CLI interface. 70 | Spawns a child process with `wkhtmltopdf `. wkhtmltopdf 71 | must be installed and path-accessible. 72 | TODO: If HTML generation has run, reuse that output 73 | TODO: Local web server to ease wkhtmltopdf rendering 74 | */ 75 | wkhtmltopdf(markup, fOut, opts, on_error) { 76 | // Save the markup to a temporary file 77 | const tempFile = fOut.replace(/\.pdf$/i, '.pdf.html'); 78 | FS.writeFileSync(tempFile, markup, 'utf8'); 79 | 80 | // Prepare wkhtmltopdf arguments. 81 | let wkopts = _.extend({'margin-top': '10mm', 'margin-bottom': '10mm'}, opts.wkhtmltopdf); 82 | wkopts = _.flatten(_.map(wkopts, (v, k) => [`--${k}`, v])); 83 | const wkargs = wkopts.concat([ tempFile, fOut ]); 84 | 85 | SPAWN('wkhtmltopdf', wkargs , false, on_error, this); 86 | }, 87 | 88 | 89 | 90 | /** 91 | Generate a PDF from HTML using Phantom's CLI interface. 92 | Spawns a child process with `phantomjs