1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2025-04-19 14:20:25 +01:00

Compare commits

..

706 Commits

Author SHA1 Message Date
hacksalot
ab6e7ee1a0
fix: create command glitch 2018-02-14 11:23:51 -05:00
hacksalot
8cccd2ffbb
chore: reset rasterize.js 2018-02-14 11:23:15 -05:00
hacksalot
c6efdeca05
chore: decaffeinate: remove generated dist/ folder 2018-02-14 10:13:48 -05:00
hacksalot
42d249b407
chore: decaffeinate: fix eslint violations 2018-02-14 10:02:44 -05:00
decaffeinate
8a46d642e5
chore: decaffeinate: convert error.coffee and 58 other files to JS 2018-02-14 09:56:31 -05:00
decaffeinate
b7cd01597e
chore: decaffeinate: rename error.coffee and 58 other files from .coffee to .js 2018-02-14 09:55:12 -05:00
hacksalot
73c5674af8
style: fix comment typo 2018-02-13 20:39:12 -05:00
hacksalot
b077ff42e4
chore: add shrinkwrap 2018-02-12 07:33:02 -05:00
hacksalot
1bc3485812
chore: set HackMyResume version to 1.9.0-beta 2018-02-12 07:11:28 -05:00
hacksalot
7597eda198
docs: merge enhancements 2018-02-12 07:05:49 -05:00
hacksalot
3badb46ae4
docs: update README images 2018-02-12 06:48:26 -05:00
hacksalot
30affe351d
chore: upate dot files 2018-02-12 05:28:02 -05:00
hacksalot
407f9f8cd7
style: fix eslint violations 2018-02-12 04:01:00 -05:00
hacksalot
922c1968ca
chore: add build-time eslint support 2018-02-12 03:34:55 -05:00
hacksalot
093a2c6a56
docs: update CHANGELOG for 1.9.0 2018-02-12 02:47:03 -05:00
hacksalot
031b666b0a Merge branch 'dev' into docs-2.0 2018-02-12 00:35:45 -05:00
hacksalot
033b29fd3a
test: remove Node 4 and 5 from Travis 2018-02-12 00:16:51 -05:00
hacksalot
c4f7350528
chore: update project dependencies 2018-02-12 00:05:29 -05:00
hacksalot
7144126175
feat: improve help behavior 2018-02-11 08:13:13 -05:00
hacksalot
a5739f337f
chore: move use.txt to help/ folder 2018-02-10 13:28:42 -05:00
hacksalot
98f20c368c
feat: introduce HELP command
Add support for a "HELP" verb in the HackMyResume CLI, allowing separate help
pages for each HackMyResume command per #205. The following command invocations
are recognized.

    hackmyresume help
    hackmyresume help build
    hackmyresume help new
    hackmyresume help convert
    hackmyresume help analyze
    hackmyresume help validate
    hackmyresume help peek
    hackmyresume help help
2018-02-10 13:03:52 -05:00
hacksalot
2281b4ea7f
chore: bump fresh-jrs-converter to 1.0.0 2018-02-10 03:02:22 -05:00
hacksalot
7196bff27c
feat: support JSON Resume edge schema 2018-02-10 01:10:20 -05:00
hacksalot
7cfdb95a04
feat: convert: support multiple JRS versions 2018-02-09 21:34:24 -05:00
hacksalot
58fe46dc83
feat: introduce FRESH version regex 2018-02-09 21:32:44 -05:00
hacksalot
17e5c6c172
style: notate an issue in skills coalescing func 2018-02-09 00:17:10 -05:00
hacksalot
ea3c72845e
chore: bump fresh-test-resumes to 0.9.2 explicit 2018-02-09 00:08:41 -05:00
hacksalot
06805578a2
chore: introduce fresh-resume-validator dependency 2018-02-09 00:07:37 -05:00
hacksalot
20815d7eff
fix: add missing require() 2018-02-09 00:06:07 -05:00
hacksalot
8648befcdd
feat: introduce stringOrObject & linkMD helpers 2018-02-07 23:35:05 -05:00
hacksalot
c08c5f0fa3
feat: introduce two skill-related helpers 2018-02-07 05:55:27 -05:00
hacksalot
38a073b09a
feat: improve template helper wiring 2018-02-07 05:49:02 -05:00
hacksalot
2346562428
fix: remove extraneous log statement 2018-02-06 08:24:18 -05:00
hacksalot
2bf5bb72cf
fix: prevent broken XML in Word docs 2018-02-06 08:22:31 -05:00
hacksalot
7262578c81
feat: allow standalone FRESH themes to inherit 2018-02-05 23:43:38 -05:00
hacksalot
66f3cb15c9
style: remove unused var 2018-02-05 23:41:40 -05:00
hacksalot
6f07141b0d
test: remove image files from freeze test 2018-02-05 00:06:14 -05:00
hacksalot
dc88073bcc
test: reactivate Awesome theme and ice check 2018-02-04 23:35:42 -05:00
hacksalot
8c81a54565
fix: resolve issues around @@@@ characters in dates
Simplify resume freezing; avoid transformations on foreign fields. Fixes #198
but needs followup to allow users to specify how and when freezing, encoding,
and transformations occur.
2018-02-04 23:06:34 -05:00
hacksalot
8dca5b76e7
refactor: remove AbstractResume base class
(1) AbstractResume adds complexity without contributing utility. There's not
really a clean "class" abstraction in JavaScript to begin with; CoffeeScript
classes, as nice as they are syntactically, occlude the issue even further.

(2) AbstractResume currently functions as a container for exactly two functions
which arguably should live outside the resume class anyway.
2018-02-04 22:49:58 -05:00
hacksalot
2767b16b47
test: use direct dependency for fresh-resume-underscore 2018-02-04 05:34:47 -05:00
hacksalot
f1343add71
test: remove hard-coded submodule path 2018-02-04 04:44:47 -05:00
hacksalot
81d9d5f157
test: add fresh-theme-underscore to suite 2018-02-04 04:06:49 -05:00
hacksalot
caca653666
style: remove unnecessary expression 2018-02-04 01:43:51 -05:00
hacksalot
55196c2766
fix: prevent weird characters in date fields 2018-02-04 01:13:02 -05:00
hacksalot
00067d012a
fix: correctly replace frozen fields in JRS-themed resumes 2018-02-03 16:15:17 -05:00
hacksalot
9da69c3310
Merge branch 'dev' into docs-2.0 2018-02-03 04:36:56 -05:00
hacksalot
02f0af1ff8
chore: update HackMyResume version to 1.9.0 2018-02-03 04:35:07 -05:00
hacksalot
b1515fc656
docs: update fresh-resume-schema link 2018-02-03 04:31:22 -05:00
hacksalot
ba719166f7
test: replace fresca with fresh-resume-schema 2018-02-02 04:49:26 -05:00
hacksalot
db6ec47dcc
chore: update stale JavaScript 2018-02-02 04:48:28 -05:00
hacksalot
f53c316ecb
chore: replace fresca with fresh-resume-schema 2018-02-02 03:42:50 -05:00
hacksalot
e889481ad8
chore: bump HackMyResume version to 2.0.0
With this upcoming release we've introduced potentially breaking functionality
so a major version bump is indicated. Additionally, it's been over a year since
the last HackMyResume release; a major version bump allows us to accept some
fruitful breakage, giving us a clean basis as we move forward on new features.
2018-02-02 02:56:59 -05:00
hacksalot
2b31f5bb58
docs: capture readme & changelog updates (interim) 2018-02-02 02:33:13 -05:00
hacksalot
7912ec9ef5
chore: add fresh-test-themes dependency 2018-02-01 22:02:50 -05:00
hacksalot
e6e0b135ed
style: clean up comments in jrs-theme.coffee 2018-02-01 19:22:17 -05:00
hacksalot
54d056c4b7
fix: exclude theme.json from interrogation 2018-02-01 17:56:43 -05:00
hacksalot
157a2a6145
chore: bump fresh-themes to 0.16.0-beta 2018-02-01 09:37:20 -05:00
hacksalot
688767d415
feat: improve ad hoc theme loading 2018-02-01 07:20:12 -05:00
hacksalot
1dbb78c53f
feat: improve custom theme helper registration 2018-02-01 07:00:59 -05:00
hacksalot
9c096541ce
feat: allow FRESH themes to specify base folder
Currently, FRESH themes contain a `src` folder that contains theme artifacts.
This commit allows the theme to specify a different folder (including "." or
""), supporting arbitrary folder structures.
2018-02-01 06:52:06 -05:00
hacksalot
5161a3a823
feat: include private fields during convert 2018-02-01 06:44:07 -05:00
hacksalot
76a386c9df
chore: bump fresh-test-resumes version to 0.9.1 2018-02-01 06:22:53 -05:00
hacksalot
7d78deec5f
test: replace 'resumes/' folder with fresh-test-resumes 2018-02-01 05:58:35 -05:00
hacksalot
a265fb633d
chore: add 'glob' dependency 2018-01-31 22:01:28 -05:00
hacksalot
069506e86d
feat: support custom theme helpers 2018-01-31 21:11:21 -05:00
hacksalot
7f656175f0
chore: bump jrs-fresh-converter to 0.2.3 2018-01-31 16:19:03 -05:00
hacksalot
94fc54174c
refactor: remove unnecessary var 2018-01-31 16:17:46 -05:00
hacksalot
231357badc
[fix] Replace legacy theme detection code. 2018-01-31 16:00:09 -05:00
hacksalot
fde2146a0b
[fix] Private fields: resolve off-by-one error [2]. 2018-01-31 15:22:15 -05:00
hacksalot
c6adab7f9e
[fix] Private fields: Resolve off-by-one error. 2018-01-31 00:10:37 -05:00
hacksalot
7f7c936897
[chore] Test: Increase resume generation timeout. 2018-01-30 13:04:08 -05:00
hacksalot
a9e35203c2
[style] Tighten syntax in html-pdf-cli-generator.coffee|js. 2018-01-30 12:40:17 -05:00
hacksalot
c913de4bf7
CONVERT: Improve command consistency. 2018-01-30 02:34:58 -05:00
hacksalot
6b125ed907 Support no-escape option for Handlebars themes. 2018-01-29 05:21:46 -05:00
hacksalot
17259cedbf Detect bad option files supplied via --options. 2018-01-29 02:04:00 -05:00
hacksalot
12a14dadeb Merge branch 'master' into dev 2018-01-28 22:38:08 -05:00
hacksalot
35fb2f5dac
Fix Travis build issues. (#204) 2018-01-28 22:34:05 -05:00
hacksalot
097e81caf8
Merge pull request #191 from ryneeverett/theme-helpers
Register handlebars helpers in themes. Fix #158.
2018-01-27 17:44:36 -05:00
hacksalot
6adf195281
Travis: Update Node.js versions. 2018-01-26 10:02:26 -05:00
hacksalot
79c304a08b Update text exemplars. 2018-01-25 15:04:49 -05:00
hacksalot
394d5cf821 Merge branch 'master' into dev 2018-01-25 14:48:35 -05:00
hacksalot
6092f985f2 Merge branch 'dev' of https://github.com/hacksalot/HackMyResume into dev 2018-01-25 14:48:14 -05:00
hacksalot
2c26c21144 Bump fresh resume versions. 2018-01-25 14:47:22 -05:00
hacksalot
6bd0b817af
Merge pull request #177 from ael-code/pdf-margins
instruct wkhtmltopdf to insert top/bottom margins
2018-01-25 13:05:35 -05:00
hacksalot
97fe171775
Merge branch 'master' into pdf-margins 2018-01-25 13:05:24 -05:00
hacksalot
9718c652ab
Merge pull request #164 from jonathanGB/master
little fix in the "Use" base case
2018-01-25 12:51:06 -05:00
hacksalot
9aad6d6138
Merge pull request #183 from peternowee/fix-issue165
Update fresh-resume-starter dependancy to 0.3.x
2018-01-25 12:44:34 -05:00
hacksalot
d25f8d0f97
Merge pull request #186 from ryneeverett/same-value-dateRange
When date ranges are identical, only show one.
2018-01-25 12:43:48 -05:00
hacksalot
d2d9039abb
Merge pull request #189 from ryneeverett/weasyprint
Add WeasyPrint pdf generator support.
2018-01-25 12:10:25 -05:00
hacksalot
3dc6ff2158
Merge pull request #184 from ryneeverett/noescape
Don't do html escaping. Fix #157.
2018-01-25 02:27:32 -05:00
hacksalot
36f5f46753
Merge pull request #192 from peternowee/fix-example-options-file
Mark options file example JSON and remove comments
2018-01-24 23:46:50 -05:00
hacksalot
8cc6334cd1 Bump FRESCA dependency to 0.6.1. 2018-01-24 21:30:19 -05:00
hacksalot
b7ef40709e Update copyright notice. 2018-01-24 21:29:47 -05:00
hacksalot
efe97ad793 Update tests.
Knock the dust off the HackMyResume test suite.
2018-01-24 21:29:24 -05:00
hacksalot
a243354044 Change admin email to admin@fluentdesk.com. 2018-01-24 21:26:45 -05:00
Peter Nowee
92c477e139 Mark options file example JSON and remove comments
The options file is JSON, not JavaScript, and JSON does not allow
comments. An options file with comments as shown in the current
example in README.md will fail to load.

This commit removes the comments. They do not seem important enough to
place them elsewhere.
2017-02-17 12:28:56 +01:00
ryneeverett
ec591b9432 Register handlebars helpers in themes. Fix #158.
Try to register all javascript files found in themes as handlebars
helpers.

Note that, unlike all other theme files currently, format directories
are ignored. I don't think there's a use case for format-specific
helpers, and this gives theme developers the flexibility to put them
either in top level files or organize them in subdirectories however
they see fit.

Note also that the theme format seems to be primarily documented in
<https://github.com/fresh-standard/fresh-themes>. This newly recognized
theme file type should be documented there should this branch be merged.
2017-01-20 22:53:16 -05:00
ryneeverett
419c935d82 Add weasyprint pdf generator support.
Note: This is based off
<https://github.com/hacksalot/HackMyResume/pull/185> because I changed
the generator expected arguments in that branch.
2017-01-16 00:15:04 -05:00
ryneeverett
d31f6caf50 When date ranges are identical, only show one. 2017-01-06 22:37:08 -05:00
ryneeverett
7e2a3c3e7e Add option to pass wkhtmltopdf options. Fix #176.
It seems that some time in the last couple years wkhtmltopdf's default
margins were changed from '10mm' to zero. As an alternative to #177,
this PR adds an option to pass in arbitrary wkhtmltopdf long arguments
and sets the default top and bottom margin to '10mm'.
2017-01-06 19:10:27 -05:00
ryneeverett
406d3358eb Don't do html escaping. Fix #157. 2016-12-14 21:14:45 -05:00
Peter Nowee
98ef625d7b Update fresh-resume-starter dependancy to 0.3.x
Fixes issue 165 (Specifying JRS as format resolves in undefined).
2016-11-26 07:29:44 +01:00
ael-code
be8a7a8361 instruct wkhtmltopdf to insert top/bottom margins
added `--margin-top 10` and `-margin-bottom 10` options in the invocation of `wkhtmltopdf`.
2016-10-21 20:03:54 +02:00
Jonathan Guillotte-Blouin
37720677f0 little fix in the "Use" base case 2016-06-27 22:04:57 -04:00
hacksalot
0cd59416b8 Remove unused dependency. 2016-04-02 15:01:28 -04:00
hacksalot
abdeeb4385 Introduce placeholder logo (interim).
Add quick 'n dirty .AI and .PNG assets for interim logo; link through
LFS.
2016-02-16 21:08:35 -05:00
hacksalot
bb7944bee7 Update resume exemplars w/ --private. 2016-02-15 16:56:00 -05:00
hacksalot
9de1156144 Update contributors.
CC @daniele-rapagnani.
2016-02-15 16:48:05 -05:00
hacksalot
9ae2703eeb Bump version to 1.9.0. 2016-02-15 16:39:37 -05:00
hacksalot
a3ed56dd15 Update JS.
Keep dist/ updated with src/ for the CLI and require() folks.
(package.json goes off dist).
2016-02-15 16:38:01 -05:00
hacksalot
214c53a3ab Merge pull request #142 from daniele-rapagnani/feature-private-fields
Implements private fields as requested in #88
2016-02-15 15:52:29 -05:00
Daniele Rapagnani
ba6b8d45f5 Reverted the compiled JS to avoid merge conflicts 2016-02-14 22:21:06 +01:00
Daniele Rapagnani
3c166a21a0 Removed the forced private option from the CONVERT verb as it is now the default behaviour 2016-02-14 22:10:30 +01:00
Daniele Rapagnani
fe46d15031 The ANALYZE command now excludes private fields by default for consistency. 2016-02-14 21:53:10 +01:00
Daniele Rapagnani
664eea752f parseJSON has been modified to always include private fields if not otherwise instructed. This is to ensure back-compatibility. The BUILD command instead, excludes private fields by default 2016-02-14 21:50:13 +01:00
Daniele Rapagnani
fed59b704e Implemented private fields that can be included or excluded with cli switch 2016-02-14 19:15:47 +01:00
hacksalot
3cf850ea0e Update test exemplars. 2016-02-14 05:32:33 -05:00
hacksalot
1b0bc87b60 Update changelog and version. 2016-02-14 04:54:44 -05:00
hacksalot
5d3b993737 Bump fresh-themes to 0.15.1-beta.
Not 100% necessary given "^" but support naive stripping of the "^"
decorator.
2016-02-14 04:32:39 -05:00
hacksalot
917fd8e3f3 Refactor helpers.
Rebind Handlebars helpers to drop the pesky options hash for standalone
helpers that don't need it. Move block helpers (which do need the
Handlebars options/context) to a separate file for special handling.
2016-02-14 04:10:23 -05:00
hacksalot
6ac2cd490b Bump fresh-test-resumes to 0.7.0. 2016-02-13 23:45:53 -05:00
hacksalot
8100190978 Bump fresh-themes to 0.15.0-beta. 2016-02-13 22:54:43 -05:00
hacksalot
7c36ff8331 Introduce "date" helper. 2016-02-13 22:54:07 -05:00
hacksalot
255a518565 Set test timeout to 30 seconds.
Most themes should generate in < 1s but allow up to 30 seconds for
network latency when opening a remote file, or for fetching remote
resources (CSS, JS, etc) during a local build.
2016-02-13 20:42:03 -05:00
hacksalot
2d595350c6 Escape LaTeX during generation. 2016-02-13 20:40:17 -05:00
hacksalot
ca92d41d9e Numerous fixes. 2016-02-13 16:08:45 -05:00
hacksalot
3f8e795c61 Fix generation glitches.
Fix output file name glitch, writing CSS files to destination folder,
and an issue where the process would evaporate before PDF/PNG generation
could complete.
2016-02-13 03:27:11 -05:00
hacksalot
9927e79900 Clean up CoffeeScript. 2016-02-13 00:40:10 -05:00
hacksalot
dbef9f0a35 Improve VALIDATE error handling. 2016-02-13 00:11:52 -05:00
hacksalot
c889664c31 More VALIDATE fixups. 2016-02-12 23:47:08 -05:00
hacksalot
7a60cd0bab Fixup VALIDATE command.
Introduce MISSING and UNKNOWN states alongside BROKEN, VALID, and
INVALID and fix regressions introduced in previous refactorings.
2016-02-12 22:49:56 -05:00
hacksalot
964350d3c7 Bump fresh-jrs-converter to 0.2.2.
Technically the "^0.2.1" implies v0.2.2 but eventually we'll drop the
"^" and use shrinkwrapped versions in dev, so explicitly bump for now.
2016-02-12 21:42:50 -05:00
hacksalot
b57d9f05af jsHint: Allow == null.
Relax jsHint's barbaric and antiquated default behavior on triple-equals
null comparisons. Allow expressive, precise, and subtle expressions such
as CoffeeScript's use of "== null" for testing against null OR undefined
under the existential operator.
2016-02-12 17:42:48 -05:00
hacksalot
b26799f9fc Improve JSON error handling.
Add support for detection of invalid line breaks in JSON string values.
Fixes #137. Could be improved to fetch the column number and drop the
messy grabbing of the line number from the exception message via regex,
but currently the "jsonlint" library (not to be confused with
"json-lint") only emits an error string. Since this is also the library
that drives http://jsonlint.com, we'll accept the messy regex in return
for more robust error checking when our default json-lint path fails.

All of the above only necessary because standard JSON.parse error
handling is broken in all environments. : )
2016-02-12 17:11:11 -05:00
hacksalot
daeffd27b5 Remove HB reference from generic helpers. 2016-02-11 22:06:43 -05:00
hacksalot
f87eb46549 Fix theme generation error. 2016-02-11 22:04:11 -05:00
hacksalot
da7cd28734 Remove unused var. 2016-02-11 22:03:49 -05:00
hacksalot
31e0bb69cc Introduce "pad()" helper.
Introduce a helper to emit padded strings / arrays of strings.
2016-02-11 22:02:50 -05:00
hacksalot
5c248cca2a Remove output folder. 2016-02-11 12:09:47 -05:00
hacksalot
f83eb018e8 Scrub tests. 2016-02-11 12:08:11 -05:00
hacksalot
317a250917 Gather. 2016-02-11 11:48:44 -05:00
hacksalot
aaa5e1fc1f Refactor generation.
Merge implicit and explicit generation paths, start emitting file
transform & copy signals, fix various bugs, introduce new bugs, support
better --debug outputs in the future.
2016-02-09 15:27:34 -05:00
hacksalot
1bc4263a46 Aerate. 2016-02-09 10:50:10 -05:00
hacksalot
e191af1fb0 Fix glitch in converted CoffeeScript.
Replace naked ternary with if then else.
2016-02-09 10:41:48 -05:00
hacksalot
7c0a9bcc02 Aerate. 2016-02-09 10:37:33 -05:00
hacksalot
d894f62607 Add ResumeFactory to facade.
Until facade is decommissioned and mothballed
2016-02-09 08:55:00 -05:00
hacksalot
2758038858 Cleanup and bug fixes.
Remove file-based open methods from resume classes; force clients to use
clean string-based or JSON overloads; fix processing glitch in
validate(); tweak outputs; adjust tests; update CHANGELOG; etc.
2016-02-04 18:49:16 -05:00
hacksalot
661fb91861 Aerate. 2016-02-04 15:23:47 -05:00
hacksalot
3c551eb923 Point package.json "main" at "dist" folder. 2016-02-04 14:38:11 -05:00
hacksalot
5bf4bda6de Fix PEEK command. 2016-02-03 20:08:17 -05:00
hacksalot
49ae016f08 Deglitch. 2016-02-02 19:02:56 -05:00
hacksalot
89957aed76 Scrub.
Adding slightly heavier function-level comments as a start for API docs.
2016-02-02 17:47:32 -05:00
hacksalot
233025ddcc Fix indentation. 2016-02-02 17:46:38 -05:00
hacksalot
11dd8952d8 Improve PEEK behavior. 2016-02-02 17:34:10 -05:00
hacksalot
d7c83613df Make CLI tests asynchronous. 2016-02-02 16:18:38 -05:00
hacksalot
a456093f13 Clean up a couple regressions. 2016-02-02 14:13:38 -05:00
hacksalot
dd4851498a Remove Resig's class implementation.
Fun while it lasted.
2016-02-02 13:49:02 -05:00
hacksalot
f72b02a0f4 Refactor generators to CoffeeScript classes. 2016-02-02 13:38:12 -05:00
hacksalot
63a0c78fc5 Refactor verbs to CoffeeScript classes.
Retire Resig's class implementation.
2016-02-01 23:16:49 -05:00
hacksalot
fd39cc9fd9 Adjust error handling / tests. 2016-02-01 22:56:08 -05:00
hacksalot
70f45d468d Asynchrony. 2016-02-01 22:52:13 -05:00
hacksalot
212b01092c Improve proc spawn behavior.
Interim until async / promises support is in.
2016-02-01 09:25:22 -05:00
hacksalot
36d641801b Add Gitter chat badge. 2016-01-31 20:02:27 -05:00
hacksalot
bd278268f6 Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2016-01-31 12:21:44 -05:00
Prayag Verma
abe31e30e0 Update license year range to 2016 2016-01-31 12:21:29 -05:00
hacksalot
314d8d8763 Introduce build instructions. 2016-01-31 12:17:17 -05:00
hacksalot
ed0792e8f8 Fix YML/JSON/PNG invalid output format warning.
Fixes #97 but we still need to support standalone PNG (ie, a PNG not
generated as part of a .all output target).
2016-01-31 09:41:00 -05:00
hacksalot
90765bf90b Refactor verb invocations to base. 2016-01-31 08:37:12 -05:00
hacksalot
f1ba7765ee Include date tests. 2016-01-30 20:20:32 -05:00
hacksalot
27c7a0264a Improve date handling. 2016-01-30 20:06:04 -05:00
hacksalot
8e806dc04f Improve duration calcs, intro base resume class. 2016-01-30 16:40:22 -05:00
hacksalot
8ec6b5ed6a Bump version to 1.7.4. 2016-01-30 12:08:02 -05:00
hacksalot
4ef4ec5d42 Remove Node. 4.5.
Travis support 4.1 and 5.0 but not 4.5.
2016-01-30 11:49:27 -05:00
hacksalot
2f523b845b Travis: Add Node 4.5. 2016-01-30 11:40:36 -05:00
hacksalot
1c416f39d3 Fix JSON Resume theme breakage.
Fixes #128.
2016-01-30 11:31:39 -05:00
hacksalot
1de0eff7b3 Merge pull request #114 from pra85/patch-1
Update license year range to 2016
2016-01-29 22:32:45 -05:00
Prayag Verma
f8a39b0908 Update license year range to 2016 2016-01-30 07:41:15 +05:30
hacksalot
d69e4635be Bump fresh-themes to 0.14.1-beta. 2016-01-29 16:14:53 -05:00
hacksalot
4b7d594502 Bump version to 1.7.3. 2016-01-29 15:50:34 -05:00
hacksalot
896b7055c1 Fix issue with undefined sections.
Fixes #127.
2016-01-29 15:50:21 -05:00
hacksalot
0f65e4c9f3 Finish HackMyCore reshaping.
Reintroduce HackMyCore, dropping the interim submodule, and reorganize
and improve tests.
2016-01-29 15:23:57 -05:00
hacksalot
e9971eb882 Bump version to 1.7.2. 2016-01-28 07:05:27 -05:00
hacksalot
beb60d4074 Integrate HMC. 2016-01-27 05:29:26 -05:00
hacksalot
4440d23584 Move HackMyCore submodule to /src. 2016-01-27 04:33:45 -05:00
hacksalot
aca67cec29 Add HMC as a submodule! 2016-01-27 04:22:41 -05:00
hacksalot
75a953aa73 Bump HackMyCore to 0.4.0. 2016-01-26 14:44:08 -05:00
hacksalot
15a0af8410 Fix output glitches. 2016-01-26 14:43:48 -05:00
hacksalot
9f811336e4 Bump HackMyCore version to 0.3.0. 2016-01-26 13:18:17 -05:00
hacksalot
a07faf6d50 ... 2016-01-26 11:43:49 -05:00
hacksalot
f098ed507f ... 2016-01-26 11:39:24 -05:00
hacksalot
80c36b96bc ... 2016-01-26 10:58:10 -05:00
hacksalot
630cf59cfb Caffeinate. 2016-01-26 06:59:34 -05:00
hacksalot
165eb5d9cd Remove extraneous console.log added by Calhoun. 2016-01-25 20:57:21 -05:00
hacksalot
d12e970af5 Exclude files from NPM. 2016-01-25 12:06:40 -05:00
hacksalot
cf18c5d90d Tweak test & clean. 2016-01-25 10:55:25 -05:00
hacksalot
0497696dcf Bump version to 1.7.1. 2016-01-25 10:41:47 -05:00
hacksalot
d007bd9bf6 Introduce CoffeeScript and build step. 2016-01-25 10:34:57 -05:00
hacksalot
5838b085c7 Fix console helpers path. 2016-01-24 18:51:08 -05:00
hacksalot
58b6ad841e Remove unused "main" entry from package.json.
Clients who were require()ing hackmyresume should now require()
hackmycore instead.
2016-01-24 17:15:32 -05:00
hacksalot
fc937e3ec8 Update "hackmyapi" references. 2016-01-24 17:14:53 -05:00
hacksalot
8652c7ecdf Rename & bump hackmyapi dependency. 2016-01-24 17:04:01 -05:00
hacksalot
c882235eff Bump version to 1.7.0. 2016-01-24 17:03:35 -05:00
hacksalot
d6c5239f9e Update roadmap.
Use linkable section headings for easier referencing.
2016-01-24 16:56:38 -05:00
hacksalot
4b2db3f720 Introduce dev roadmap. 2016-01-24 16:11:56 -05:00
hacksalot
9736777828 Fix Travis. 2016-01-24 10:53:32 -05:00
hacksalot
d3194fba19 Relocate internal sources to HackMyAPI.
Move internal sources and related tests to:

https://github.com/hacksalot/HackMyAPI
2016-01-24 09:55:04 -05:00
hacksalot
fa29f9794d Update README image. 2016-01-24 06:23:15 -05:00
hacksalot
07915002bb Adjust "merging X onto Y" output. 2016-01-24 05:35:07 -05:00
hacksalot
fbcc06dcda Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2016-01-24 05:17:24 -05:00
Timothy Beery
7413a3a257 Fixed type 2016-01-24 05:17:02 -05:00
hacksalot
e6d2255291 Scrub. 2016-01-23 23:30:48 -05:00
hacksalot
2840ec3f87 Introduce {{fontSize}} helper. 2016-01-23 22:40:33 -05:00
hacksalot
05cd863ebf Add PDF engines to man page. 2016-01-23 20:30:23 -05:00
hacksalot
20961afb62 Introduce {{color}} helper. 2016-01-23 20:24:35 -05:00
hacksalot
1256095e25 Support "fonts.all" in FRESH themes.
Add support for default font specs in FRESH theme.json files. The "all"
format matches any format that doesn't have a specific key in "fonts".
2016-01-23 03:58:11 -05:00
hacksalot
f073c79b8d Better dynamic font handling. 2016-01-22 22:19:28 -05:00
hacksalot
ac9e4aa1a0 Capture CHANGELOG and FAQ. 2016-01-22 20:00:07 -05:00
hacksalot
915f35b1e6 Improve Underscore.js rendering support. 2016-01-22 10:36:26 -05:00
hacksalot
4fe74057f9 Improve font helpers.
Log a warning on incorrect use.
2016-01-22 08:33:01 -05:00
hacksalot
5a1ec033bb Adjust USE.txt.
--opts has changed to --options and --no-tips to --tips.
2016-01-22 08:27:21 -05:00
hacksalot
6801e39f97 Tweak output colorization. 2016-01-22 04:55:29 -05:00
hacksalot
f6f383751f Fix JSON Resume theme rendering glitch. 2016-01-22 03:05:41 -05:00
hacksalot
43ed564a6e Disable tips and theme messages by default.
Instead of displaying tips by default and allowing users to turn them
off with --no-tips, hide tips by default and allow users to show them
with --tips.
2016-01-22 02:51:00 -05:00
hacksalot
7b3364c356 Document parameter. 2016-01-22 02:44:17 -05:00
hacksalot
58a7fc09e5 Add toUpper helper. 2016-01-22 02:44:04 -05:00
hacksalot
01c053702d Gather. 2016-01-21 23:40:15 -05:00
hacksalot
a935fe7dc2 Introduce {{fontFace}} helper. 2016-01-21 23:39:30 -05:00
hacksalot
c9825fa016 Update .gitignore. 2016-01-21 23:23:24 -05:00
hacksalot
9eb9207348 Mention string.prototype.endswith dependency. 2016-01-21 05:41:39 -05:00
hacksalot
6b171e69db Improve CSS handling. 2016-01-21 05:21:49 -05:00
hacksalot
5b0ee89e34 Bump FRESCA to 0.6.0. 2016-01-20 21:44:01 -05:00
hacksalot
8bd3ddc7fd Bump fresh-resume-starter to 0.2.2. 2016-01-20 21:43:49 -05:00
hacksalot
984ae95576 Cleanup. 2016-01-20 21:43:11 -05:00
hacksalot
f77cced7f3 Improve error handling. 2016-01-20 19:59:36 -05:00
hacksalot
57787f1bc7 Re-introduce API-level tests. 2016-01-20 01:48:57 -05:00
hacksalot
9419f905df Build verb invocation should return JSON result. 2016-01-20 01:48:33 -05:00
hacksalot
001fd893c1 Add tests for partial resume loading (FRESH). 2016-01-20 00:21:07 -05:00
hacksalot
babe4b4b31 Bump versions. 2016-01-20 00:20:10 -05:00
hacksalot
201f39442e Add support for .ignore flag in FRESH and JRS resumes.
Preliminary support for ".ignore" on any non-leaf FRESH or JRS node.
Nodes (employment entries, education stints, etc.) decorated with
".ignore" will be treated by HMR as if they weren't present.
2016-01-19 20:09:59 -05:00
hacksalot
47f6aff561 Improve keyword regex.
Better support for simple keywords like "C" or "R".
2016-01-19 19:10:20 -05:00
hacksalot
cef9a92cb6 Introduce CHANGELOG.md. 2016-01-19 16:42:48 -05:00
hacksalot
2253e4ead7 Fix theme counts.
The N in "Applying theme FOOBAR (N formats)" should reflect the count of
explicit + freebie output formats.
2016-01-19 16:01:34 -05:00
hacksalot
2f628f8564 Reconnect process exit codes. 2016-01-18 20:06:45 -05:00
hacksalot
23cd52885b Swallow inline failures in CONVERT. 2016-01-18 19:21:25 -05:00
hacksalot
181419ae28 Improve PEEK command behavior. 2016-01-18 19:20:17 -05:00
hacksalot
a81ad0fef2 Tweak build command error condition. 2016-01-18 18:36:24 -05:00
hacksalot
d220cedfeb Improve behavior of PEEK command. 2016-01-18 18:35:38 -05:00
hacksalot
e72564162b Remove custom "extend" method.
Replace with NPM extend.
2016-01-18 17:31:08 -05:00
hacksalot
c98d05270e Improve error handling. 2016-01-18 17:13:37 -05:00
hacksalot
3e3803ed85 Improve error handling. 2016-01-18 14:10:35 -05:00
hacksalot
c8d8e566f8 Add IIFE. 2016-01-18 14:10:25 -05:00
hacksalot
712cba57b8 Capture. 2016-01-18 00:34:57 -05:00
hacksalot
c9e45d4991 Capture. 2016-01-17 21:46:58 -05:00
hacksalot
e9edc0d15c Bump fresh-test-resumes to 0.5.0. 2016-01-16 16:34:25 -05:00
hacksalot
b99a09c88a Integrate tests with new fresh-jrs-converter structure. 2016-01-16 16:29:00 -05:00
hacksalot
5c95fe7af1 Integrate with fresh-jrs-converter.
Move FRESH/JRS conversion logic (and all future format conversions) into
a separate repo.
2016-01-16 12:40:16 -05:00
hacksalot
17f2ebb753 Modularize messages.
...and move strings out of error.js.
2016-01-15 23:46:43 -05:00
hacksalot
fc67f680ee Move output messages to YAML. 2016-01-15 22:52:10 -05:00
hacksalot
88879257e6 Document PEEK command.
Add preliminary docs around PEEK.
2016-01-15 14:46:13 -05:00
hacksalot
fff45e1431 Update tests. 2016-01-15 13:36:57 -05:00
hacksalot
934d8a6123 Update --options file loading. 2016-01-15 13:36:20 -05:00
hacksalot
defe9b6e95 Remove magic number. 2016-01-15 13:35:45 -05:00
hacksalot
4c5ccc001a Introduce PEEK command.
Peek at arbitrary resumes and resume objects paths with "hackmyresume
peek <resume> [objectPath]". For ex:

hackmyresume PEEK resume.json
hackmyresume PEEK resume.json info
hackmyresume PEEK resume.json employment[2].keywords
hackmyresume PEEK r1.json r2.json r3.json info.brief
2016-01-15 13:08:01 -05:00
hacksalot
de5c2ecb95 Update dependencies. 2016-01-14 23:39:54 -05:00
hacksalot
dbb95aef3a Bump fresh-resume-starter / fresh-test-resumes. 2016-01-14 15:53:03 -05:00
hacksalot
c9ae2ffef3 Improve errors / tests consistency. 2016-01-14 14:22:26 -05:00
hacksalot
86af2a2c4f Rename test-cli.js to test-api.js. 2016-01-14 12:07:43 -05:00
hacksalot
37ea6cf804 Rename error-handler.js to error.js. 2016-01-14 11:49:27 -05:00
hacksalot
a9c685c6a4 Refactor error handling (interim). 2016-01-14 11:47:05 -05:00
hacksalot
7765e85336 Integrate printf(). 2016-01-14 09:46:29 -05:00
hacksalot
7af50c51f6 Gather. 2016-01-14 08:48:07 -05:00
hacksalot
19b30d55ec Move error handling out of core. 2016-01-13 15:28:02 -05:00
hacksalot
eddda8146e Bump FRESCA and fresh-themes versions. 2016-01-13 14:46:35 -05:00
hacksalot
1a0b91a58f Update conversion tests. 2016-01-12 18:14:06 -05:00
hacksalot
1b94ada709 Misc improvements. 2016-01-12 18:13:54 -05:00
hacksalot
1966b0a862 Move string transformation out of FRESHResume. 2016-01-12 13:28:20 -05:00
hacksalot
8ced6a730a Fix BUILD command event notifications. 2016-01-12 12:46:55 -05:00
hacksalot
6cd1e60e79 Sort projects. 2016-01-12 12:46:18 -05:00
hacksalot
be691e4230 Remove commented lines. 2016-01-12 12:46:05 -05:00
hacksalot
07b23109f9 Use async spawn() by default. 2016-01-12 12:32:32 -05:00
hacksalot
32769a2b0b Update license to 2016. 2016-01-11 21:16:11 -05:00
hacksalot
280977cb62 Update package.json contributors. 2016-01-11 21:16:01 -05:00
hacksalot
ddceec68a2 Improve --options tests. 2016-01-11 21:15:28 -05:00
hacksalot
b961fd1c07 Fix global leak. 2016-01-11 21:14:40 -05:00
hacksalot
342b960f63 Add tests for raw JSON and file via --options / -o. 2016-01-11 20:52:17 -05:00
hacksalot
f965bf456a Fix JSON file loading glitch with --options. 2016-01-11 20:52:07 -05:00
hacksalot
69be38110f Update license notice in index.js. 2016-01-11 19:56:44 -05:00
hacksalot
3800e19418 Process TXT global partials. 2016-01-11 19:56:19 -05:00
hacksalot
e29ed58a1c Tests: Update theme name. 2016-01-11 18:08:31 -05:00
hacksalot
11bfcd4bef Support raw JSON in the --options parameter. 2016-01-11 18:07:56 -05:00
hacksalot
fbc2e9a4db Bump version to 1.6.0. 2016-01-11 14:04:05 -05:00
hacksalot
7814786957 Recruit Markdown partials when present. 2016-01-11 12:36:00 -05:00
hacksalot
542776fd2e Add shortcut options to man page. 2016-01-11 08:31:05 -05:00
hacksalot
815ee3dc7e Support lowercase -v version flag.
Commander.js built-in version handling uses an uppercase shortcut (-V)
for the version, so the common -v (lowercase) isn't recognized and
errors out.
2016-01-11 08:29:46 -05:00
hacksalot
376e720f4b Scrub. 2016-01-11 08:21:06 -05:00
hacksalot
b224c8939b Remove redundant conditional. 2016-01-11 08:20:48 -05:00
hacksalot
0ecac98cff Remove totally unnecessary line.
Totally.
2016-01-10 19:11:43 -05:00
hacksalot
1416f57d0b Move verb.js to /verbs folder. 2016-01-10 19:08:29 -05:00
hacksalot
65c7e41c53 Remove unused var. 2016-01-10 19:02:24 -05:00
hacksalot
c8cc673ad5 Update man page. 2016-01-10 18:48:57 -05:00
hacksalot
656dbe2fc2 Capture. 2016-01-10 14:53:22 -05:00
hacksalot
a4ee7127ee Fix stack reporting glitch. 2016-01-10 13:28:20 -05:00
hacksalot
fee21a7b17 Always use JSONLint for SyntaxError post-processing.
Remove the check for SyntaxError's built-in line and character
indicators and always re-parse on error to grab the line/column.
2016-01-10 05:17:28 -05:00
hacksalot
32fd8dc636 Merge pull request #102 from beeryt/master
Fixed typo
2016-01-10 02:27:03 -05:00
Timothy Beery
2c8f444d42 Fixed type 2016-01-09 21:12:19 -08:00
hacksalot
bd8b587c5b Remove explicit logger and error handler params. 2016-01-09 22:34:21 -05:00
hacksalot
4c954b79df Scrub. 2016-01-09 22:15:50 -05:00
hacksalot
b7fffbcf73 Update helper reference in analysis .hbs. 2016-01-09 22:14:34 -05:00
hacksalot
0829800b65 Move helpers to /helpers. 2016-01-09 22:13:29 -05:00
hacksalot
d7cfc76636 Promote console helpers has to console-helpers.js. 2016-01-09 22:11:06 -05:00
hacksalot
311030474d Tests: Remove hard-coded version number. 2016-01-09 20:29:30 -05:00
hacksalot
ec69e668ff Bump version to 1.5.3. 2016-01-09 20:21:17 -05:00
hacksalot
f18910f490 Generate ANALYZE console output from Handlebars template. 2016-01-09 20:18:56 -05:00
hacksalot
540ad48d61 Scrub. 2016-01-09 16:56:30 -05:00
hacksalot
540c745069 Exclude Emacs cruft. 2016-01-09 16:44:00 -05:00
hacksalot
c5b8eec33a Move CLI-related assets to subfolder. 2016-01-09 16:14:28 -05:00
hacksalot
bece335a64 Fix CREATE verb output. 2016-01-09 15:58:39 -05:00
hacksalot
3aabb5028d Continue moving logging out of core. 2016-01-09 15:49:08 -05:00
hacksalot
732bc9809a Start moving logging out of core. 2016-01-09 13:58:47 -05:00
hacksalot
d77b484e55 Verbs are event emitters.
Let verbs source events through EventEmitter. Using aggregation is a bit
simpler here than extending because of the Resig "Class" stuff.
2016-01-09 08:12:55 -05:00
hacksalot
43564bf380 Update tests. 2016-01-09 06:44:47 -05:00
hacksalot
88c71f6e9c Move commands to Verb hierarchy
Move flat command functions (BUILD, ANALYZE, etc.) to a shallow Verb
hierarchy. Allow command verbs to inherit common functionality and prep
for better debugging/logging as well as test mocks.
2016-01-09 06:44:22 -05:00
hacksalot
47e8605f50 Handle args in mock/passthrough case. 2016-01-09 05:30:12 -05:00
hacksalot
9466a8c0dd Remove spawn-watch.
No longer necessary.
2016-01-09 05:29:45 -05:00
hacksalot
d878270bc6 Encapsulate CLI interface to ease testing.
Strip index.js down to its bare essentials, move primary logic to
main.js, and expose the latter via module.exports. This allows tests to
execute the same code path(s) HMR runs in production.
2016-01-08 19:22:44 -05:00
hacksalot
3b38c4818f Bump version. 2016-01-08 18:56:07 -05:00
hacksalot
62c967526f Fix PDF exception glitch. 2016-01-08 18:15:12 -05:00
hacksalot
6e5a44798b Update README. 2016-01-08 16:36:19 -05:00
hacksalot
1fbfe2507b Carry over debug flag. 2016-01-08 16:33:13 -05:00
hacksalot
d6a3aab68a Make Handlebars options explicit. 2016-01-08 16:27:19 -05:00
hacksalot
9fdfd1b5a6 Add baseline support for -d or --debug flag.
For now, -d just force-emits the stack when there is one. In the future,
it can trigger more detailed logging info.
2016-01-08 16:08:33 -05:00
hacksalot
f4e763bd9c Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2016-01-08 12:28:45 -05:00
Antonio Ruberto
fbfff2a4e4 load theme partials for non html and doc
load global partials for html and doc only but load theme partials for
all outputs
2016-01-08 12:28:23 -05:00
hacksalot
1c93932737 Fix jsHint error. 2016-01-08 12:24:23 -05:00
hacksalot
cba29511bc Analyze: fix coverage percentage glitch. 2016-01-08 12:20:51 -05:00
hacksalot
1d655a4ddb Support duration units for JRS resumes. 2016-01-08 12:13:54 -05:00
hacksalot
ca94513630 Fix single format output error.
Fixes #97.
2016-01-08 11:59:10 -05:00
hacksalot
971d4a5439 Update FAQ and README. 2016-01-08 11:48:10 -05:00
hacksalot
f3dcbd9081 Improve error vs. warning formatting.
Errors = red. Warnings = yellow.
2016-01-08 10:42:24 -05:00
hacksalot
29c53af843 Rename "invalidTarget" error to "invalidFormat". 2016-01-08 10:09:46 -05:00
hacksalot
8d24087faa Rename src/gen --> src/generators. 2016-01-08 10:02:47 -05:00
hacksalot
95df8e5af4 Rename src/eng --> src/renderers
A renderer is a thing that renders or "paints" an arbitrary format using
a templating engine like Handlebars or Underscore. A generator is a
thing responsible for generating a given output format like HTML or MS
Word.
2016-01-08 09:59:47 -05:00
hacksalot
8a1da777b0 Bump version to 1.5.0. 2016-01-08 09:38:53 -05:00
hacksalot
44555da00f Fix PNG output format for JSON Resume themes. 2016-01-08 09:36:32 -05:00
hacksalot
46bd5d51cc Support implicit PDF generation (interim). 2016-01-08 09:00:43 -05:00
hacksalot
3964d300aa Update README. 2016-01-08 08:59:43 -05:00
hacksalot
d6280e6d89 Start integrating JRS and FRESH rendering paths. 2016-01-08 08:40:19 -05:00
hacksalot
4a2a47f551 Tweak casing. 2016-01-08 07:08:12 -05:00
hacksalot
ae51930c9c Tweak indentation. 2016-01-08 07:06:26 -05:00
hacksalot
fb33455bea Refactor JRS rendering. 2016-01-08 06:48:04 -05:00
hacksalot
28c703daf7 Improve error handling: PDFs. 2016-01-08 05:11:38 -05:00
hacksalot
0246a5da19 Remove html-pdf-generator class.
PDF generation now performed via html-pdf-cli-generator.
2016-01-07 18:34:43 -05:00
hacksalot
840d17c67b Wrap rasterize.js in IIFE / satisfy jsHint. 2016-01-07 18:33:26 -05:00
hacksalot
9f22e94cf7 Merge pull request #95 from aruberto/partials-fix
load theme partials for non html and doc
2016-01-07 18:30:54 -05:00
hacksalot
97ebecd84a Support CLI-based PDF generation.
Support Phantom and wkhtmltopdf generation via CLI.
2016-01-07 18:24:25 -05:00
hacksalot
96b9bb68e3 Introduce Phantom.js rasterizer script.
Via
https://raw.githubusercontent.com/ariya/phantomjs/master/examples/rasterize.js.
2016-01-07 17:53:42 -05:00
hacksalot
c5a5d3761d Remove explicit Phantom and wkhtmltopdf dependency.
Phantom is too heavy to impose on casual users and wkhtmltopdf errors
out on half the systems out there. We're better off speaking to both
tools, when present, via CLI or a secondary script.
2016-01-07 16:47:59 -05:00
Antonio Ruberto
c147403b1c load theme partials for non html and doc
load global partials for html and doc only but load theme partials for
all outputs
2016-01-07 16:39:46 -05:00
hacksalot
a2723452c2 Improve ENOENT handling. 2016-01-07 16:13:09 -05:00
hacksalot
cb3488276d Refactor error handling.
Work towards better debug/log/stack trace options for error cases.
2016-01-07 15:54:10 -05:00
hacksalot
43419c27cf Refactor API surface. 2016-01-07 13:44:39 -05:00
hacksalot
0f0c399dd5 Update CLI tests. 2016-01-07 13:12:21 -05:00
hacksalot
cb46497346 Rename generate.js to build.js.
Should match the canonical verb name -- "build". Generate is an alias.
2016-01-07 12:03:44 -05:00
hacksalot
850c640368 Annotate Phantom gen method. 2016-01-07 10:54:46 -05:00
hacksalot
60e455b36d Emit call stack for wkhtmltopdf errors. 2016-01-07 10:54:27 -05:00
hacksalot
af896c85ea Bump version to 1.4.2. 2016-01-07 02:06:55 -05:00
hacksalot
6a7bb5ea5b Update README. 2016-01-07 01:09:48 -05:00
hacksalot
3b6f2ad37e Introduce FAQ.
Use a separate Markdown document instead of the GH wiki so that the FAQ
is present after clone and advertises itself in the root folder.
2016-01-07 00:58:40 -05:00
hacksalot
101eebdd95 Update tests. 2016-01-06 14:17:27 -05:00
hacksalot
830c36818e Tweak missing file message for "new" command. 2016-01-06 14:15:27 -05:00
hacksalot
39e995213f Improve starter resume.
"hackmyresume new" should emit a starter resume that a) has example
information and b) validates.
2016-01-06 14:09:22 -05:00
hacksalot
37a053722d Update Travis URLs. 2016-01-06 11:36:40 -05:00
hacksalot
12fcf3b0cb Fix package.json glitch. 2016-01-06 11:28:09 -05:00
hacksalot
43ad9c1c71 Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2016-01-06 11:24:02 -05:00
Josh Janusch
4f9207a868 Fix: formatDate helper references the moment method, not the momentDate object 2016-01-06 11:23:39 -05:00
Josh Janusch
3d1f589bc1 formatDate helper now will only use moment if date is valid. If it's not, will use the user inputted value or a fallback parameter, if it is provided 2016-01-06 11:23:38 -05:00
hacksalot
ae436a3b84 Scrub. 2016-01-06 11:18:50 -05:00
hacksalot
202bb44c76 Update contributors.
@robertmain @jjanusch
2016-01-06 11:18:31 -05:00
hacksalot
041c609ff0 Merge pull request #85 from jjanusch/feat/present-formatter
formatDate template helper error handling and fallback
2016-01-06 11:00:17 -05:00
hacksalot
712b504168 Support global theme partials (interim). 2016-01-06 10:48:51 -05:00
hacksalot
bc9f0d468f Update tests w/ new validation behavior. 2016-01-06 00:44:46 -05:00
hacksalot
2d20077c08 Support --assert option for validate command.
Cause HMR to return an error code if validation fails and the --assert
option is present.
2016-01-06 00:44:34 -05:00
hacksalot
f61deda4e8 Fix format detection error in validate logic. 2016-01-06 00:21:18 -05:00
hacksalot
8203fa50ae Prep convert.js. 2016-01-06 00:20:30 -05:00
hacksalot
c5eab0fd9c Scrub. 2016-01-05 23:59:41 -05:00
hacksalot
40e71238ac Scrub. 2016-01-05 23:46:01 -05:00
hacksalot
9d75b207d1 Formalize empty-fresh.json dependency. 2016-01-05 23:28:49 -05:00
hacksalot
9b52c396d3 Fix missing method rename. 2016-01-05 22:32:46 -05:00
hacksalot
2759727984 Add convenience method. 2016-01-05 22:26:16 -05:00
hacksalot
e230d640cb Rename imp() to i() (interim). 2016-01-05 22:02:11 -05:00
hacksalot
d69688697c Update README. 2016-01-05 19:48:11 -05:00
hacksalot
9f7ec62b18 Bump fresh-themes to 0.11.0-beta. 2016-01-05 10:26:29 -05:00
hacksalot
b1a02918ff Support --no-tips flag. 2016-01-05 10:10:24 -05:00
hacksalot
ec05f6737a Emit JSON Resume theme instructions. 2016-01-05 10:10:12 -05:00
hacksalot
da5db6477b Introduce --color and --no-color options.
These are handled by Chalk, but need to be registered with Commander.js
in order for Chalk to see them.
2016-01-05 09:42:39 -05:00
hacksalot
0f580efb2b Mention ANALYZE command in man page. 2016-01-05 09:38:42 -05:00
hacksalot
ff23ee508b Restore app title. 2016-01-05 09:38:21 -05:00
hacksalot
2819faeb6f Improve theme/format inheritance (interim). 2016-01-05 09:28:40 -05:00
hacksalot
d205e882f6 Introduce FRESH theme/format inheritance.
Support "inherits" property in theme.json (FRESH themes only).
2016-01-05 06:34:56 -05:00
hacksalot
3f40441b0a Bump FRESCA version to 0.3.0. 2016-01-05 05:09:29 -05:00
hacksalot
6185f20ec9 Sort project history by default. 2016-01-05 05:00:04 -05:00
hacksalot
6a61989eb4 Introduce {{dateRange}} and {{camelCase}} helpers. 2016-01-05 04:59:51 -05:00
hacksalot
d658a069cd Rename {{hasSection}} helper to {{section}}. 2016-01-05 04:59:26 -05:00
hacksalot
25688dbe8e Bump fresh-test-resume to 0.2.1. 2016-01-05 01:41:04 -05:00
hacksalot
98362b9687 Bump fresh-test-resumes version. 2016-01-05 00:04:10 -05:00
hacksalot
4c31c96891 Introduce has/hasSection helpers. 2016-01-05 00:03:54 -05:00
hacksalot
219209c6ca Fix logic glitch in {{sectionTitle}} helper. 2016-01-04 19:46:45 -05:00
hacksalot
eff9fc51cb Integrate fresh-test-resumes module. 2016-01-04 19:45:49 -05:00
hacksalot
2ba23ee80d Add support for user-definable section titles.
Introduce a {{sectionTitle}} helper; requires theme updates.
2016-01-04 16:20:48 -05:00
hacksalot
0f83f8f5c2 Merge remote-tracking branch 'refs/remotes/origin/master' into dev 2016-01-04 16:13:37 -05:00
hacksalot
4ba3a3f2a9 Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2016-01-04 08:09:12 -05:00
hacksalot
db486a48aa Update README. 2016-01-04 08:06:51 -05:00
hacksalot
2cab1195e8 Fix 'create' alias. 2016-01-04 07:25:48 -05:00
hacksalot
ce75f09210 Refactor API interface. 2016-01-04 07:23:20 -05:00
hacksalot
a8fed1b69b Add missing semicolon. 2016-01-04 04:15:13 -05:00
hacksalot
62ca2020d8 Bump FRESH themes version. 2016-01-04 04:15:01 -05:00
hacksalot
f65cf8880e Add support for external options file. 2016-01-04 02:50:00 -05:00
hacksalot
c8d4a3deb3 Handle global options.
Fix broken --silent flag and set up -o/-opts.
2016-01-04 01:49:35 -05:00
hacksalot
d5e2a45034 Output theme message on generate. 2016-01-04 00:58:41 -05:00
hacksalot
2465f2ce1c Fix gap analysis glitches. 2016-01-04 00:14:43 -05:00
hacksalot
f2001bcbb1 Add a couple baseline "analyze" tests. 2016-01-03 23:18:35 -05:00
hacksalot
d5afb3eb2e Handle missing dates during gap inspection. 2016-01-03 23:17:36 -05:00
hacksalot
c711cb7922 Improve sorting. 2016-01-03 23:17:18 -05:00
hacksalot
e45e0316f6 Remove extraneous regex. 2016-01-03 10:07:58 -05:00
hacksalot
08ab512f4c Add overlap analysis. 2016-01-03 09:48:43 -05:00
hacksalot
f2bf09bf96 Allow variable-unit resume duration. 2016-01-03 09:48:22 -05:00
hacksalot
75e2b1c131 Improve keyword acquisition. 2016-01-03 09:48:02 -05:00
hacksalot
0b7ef16a41 Improve accuracy of keyword counts. 2016-01-03 07:36:05 -05:00
hacksalot
247eec396c Fix string iteration filtering glitch. 2016-01-03 07:35:47 -05:00
hacksalot
46c7fa9838 Add baseline keyword analysis. 2016-01-03 06:39:46 -05:00
hacksalot
b3fb2c7130 Scrub. 2016-01-03 05:06:54 -05:00
hacksalot
c3ec3f28bd Introduce section totals inspector. 2016-01-03 05:03:31 -05:00
hacksalot
0a8ee721e8 Allow for multiple PDF engines / support Phantom PDFs.
Start formalizing PDF generation apparatus and support a `--pdf`
parameter allowing the user to specify the flavor of PDF generation.
2016-01-03 04:11:42 -05:00
hacksalot
8d7cf32988 Finish Commander.js integration. 2016-01-03 03:18:56 -05:00
hacksalot
655ecebaa5 Clean up comments. 2016-01-03 02:40:04 -05:00
hacksalot
8fc0fa99d3 Remove unnecessary indirection. 2016-01-03 02:39:43 -05:00
hacksalot
69e8adc1cc Remove 'minimist' dependency. 2016-01-03 02:25:39 -05:00
hacksalot
6b3396e01b Use Commander.js for invocations. 2016-01-03 02:22:26 -05:00
hacksalot
a95b52acd0 Refactor command processing. 2016-01-02 00:15:46 -05:00
hacksalot
47553b6def Fix ICE encoding issues.
Fix issue where @@@@ is appearing in generated resumes.
2016-01-01 20:27:46 -05:00
hacksalot
e4a549ed30 Tests: Add ICE detection test.
ICE is the internal boilerplate we use to freeze/unfreeze themes when
trying to force-feed them Markdown or other formatted data.
2016-01-01 20:26:47 -05:00
Josh Janusch
dd2148bb92 Fix: formatDate helper references the moment method, not the momentDate object 2016-01-01 18:05:33 -05:00
hacksalot
d8b9d86896 Scrub. 2016-01-01 17:30:57 -05:00
Josh Janusch
889bd4bfc5 formatDate helper now will only use moment if date is valid. If it's not, will use the user inputted value or a fallback parameter, if it is provided 2016-01-01 17:27:49 -05:00
hacksalot
13fc903b2b Catch JSON syntax errors for all commands.
...and emit line/column info.
2016-01-01 17:20:42 -05:00
hacksalot
8c8dbfed72 Adjust test paths. 2016-01-01 15:06:36 -05:00
hacksalot
2b669cf35c Tweak error handling for cmd params. 2016-01-01 15:06:16 -05:00
hacksalot
5a2d892b85 Scrub error-handler.js. 2016-01-01 14:59:21 -05:00
hacksalot
37a7c318d5 Remove stack trace for ENOENT. 2016-01-01 14:58:56 -05:00
hacksalot
43873efcab Tweak analyze command error. 2016-01-01 14:38:52 -05:00
hacksalot
bb28e5aa8e Support --help option.
Support standard syntax for the HELP command.
2016-01-01 14:38:00 -05:00
hacksalot
c17261cd25 Merge pull request #81 from tjlav5/master
Fix relative theme directory
2016-01-01 13:46:55 -05:00
TJ Lavelle
49e56cc226 Fix relative theme directory
The theme directory assumes it was a child of the HackMyResume module, but NPM3 will actually flatten this out. Following the same logic that the template-generator uses, find the path to the themes using NPMs require method.
2016-01-01 11:27:05 -05:00
hacksalot
84ad6cf356 Add missing chalk references. 2016-01-01 04:57:50 -05:00
hacksalot
b96526da31 Replace chalk with colors in tests. 2016-01-01 04:48:20 -05:00
hacksalot
cb14452df3 Replace colors with chalk.
Chalk has a few more options and doesn't mess around with
String.prototype.
2016-01-01 04:44:14 -05:00
hacksalot
d54b9a6d6c Remove unused method. 2016-01-01 03:45:14 -05:00
hacksalot
6285c2db3b Introduce "analyze" verb and framework.
Introduce a new "analyze" command and start setting up the inspector /
analyzer pipeline with a simple "gap analysis" inspector using a
reference-counted gap detection approach.
2016-01-01 03:39:48 -05:00
hacksalot
3453293c79 Bump version to 1.3.1. 2015-12-31 20:41:54 -05:00
hacksalot
fb32cb0d78 Tests: Bump Johnny's expected duration to 4 years.
Happy New Year, everybody.
2015-12-31 20:00:39 -05:00
hacksalot
baccb75256 Tests: fix Travis error on Node 0.10.
Node 0.10 doesn't have path.parse, so use require('parse-filepath') as a
workaround.
2015-12-31 19:51:06 -05:00
hacksalot
5c39c1c93d Remove extraneous console.log. 2015-12-31 19:47:55 -05:00
hacksalot
48cc315fc8 Update Travis shields.
Add version and a badge for the /dev branch.
2015-12-31 19:17:56 -05:00
hacksalot
ea8da6811a Include Node 0.10 in Travis tests.
We've already done some work to support legacy Node 0.10 (ex
https://github.com/hacksalot/HackMyResume/issues/31#issuecomment-167155845)
no reason to drop this support by omitting tests.
2015-12-31 18:31:39 -05:00
hacksalot
dbda48c16d Add additional validate tests. 2015-12-31 18:24:45 -05:00
hacksalot
bc710b5c6e Merge pull request #77 from hacksalot/dev
v1.3.0 changes
2015-12-31 06:43:34 -05:00
hacksalot
b85d40b1b3 Improve XML encoding for Word docs.
Fix various encoding errors.
2015-12-31 06:38:30 -05:00
hacksalot
069c02ddcc Interim changes supporting v1.3.0. 2015-12-31 03:34:41 -05:00
hacksalot
1f6d77fc28 Bump version to 1.3.0. 2015-12-31 03:18:02 -05:00
hacksalot
2b4266ee42 Merge pull request #69 from zhuangya/missing-extend-def-fix-68
fix: missing extend method
2015-12-30 22:11:20 -05:00
hacksalot
2b3c83c57e Merge branch 'master' of https://github.com/hacksalot/HackMyResume 2015-12-30 22:03:38 -05:00
hacksalot
6f37ccdee3 Merge pull request #75 from hacksalot/dev
Changes for v1.3.0-beta
2015-12-30 21:26:27 -05:00
hacksalot
df27924ac2 Add Johnny Trouble to tests. 2015-12-30 21:07:28 -05:00
hacksalot
3cf24cfb40 Fix PNG generation glitch. 2015-12-30 20:11:21 -05:00
hacksalot
3acf648eb4 Expose helpers to Underscore engine.
Get the same set of helpers working for Underscore and Handlebars
engines. Needs refactoring.
2015-12-30 20:11:09 -05:00
hacksalot
76cafa4249 Fix reference error in explicit themes. 2015-12-30 20:10:14 -05:00
hacksalot
55943bf49a Fix missing semicolon. 2015-12-30 20:09:39 -05:00
hacksalot
a280d8acb2 Support CSS embedding vs. linking. 2015-12-30 19:45:50 -05:00
hacksalot
558a321fe8 Refactor generator logic. 2015-12-30 18:52:41 -05:00
hacksalot
d901047043 Update fluent-themes --> fresh-themes. 2015-12-30 18:50:58 -05:00
hacksalot
d4e0a0fa05 Add {{styleSheet}} helper (placeholder). 2015-12-30 18:19:00 -05:00
hacksalot
22554c61c5 Rename and bump fluent-themes dependency. 2015-12-30 18:18:11 -05:00
hacksalot
72de1bbd33 Scrub. 2015-12-30 15:21:58 -05:00
hacksalot
2ff912e687 Scrub. 2015-12-30 15:11:18 -05:00
hacksalot
ccadb0416f Move freebie formats out of theme class. 2015-12-30 15:03:26 -05:00
hacksalot
5e51beddf7 Refactor. 2015-12-30 14:48:22 -05:00
hacksalot
97c9ba08d0 Fix: Broken HELP command. 2015-12-30 14:00:09 -05:00
hacksalot
39d61c66b9 Finish Theme --> FreshTheme rename. 2015-12-30 13:22:18 -05:00
hacksalot
7a1eadb3fc Tweak error messages.
Stay away from language like "please specify a valid input resume". The
fluentcv fork can use corporate-speak. HackMyResume is more like a
gremlin -- feed it, but never after midnight.
2015-12-30 13:12:51 -05:00
hacksalot
1bcc2f7d0c Add formal support for aliases.
new/create and build/generate
2015-12-30 13:00:30 -05:00
hacksalot
e3cb949992 Fix: Exception when HMR is run without params. 2015-12-30 12:59:21 -05:00
hacksalot
a0c356941c Remove unnecessary line. 2015-12-30 12:44:16 -05:00
hacksalot
3c7868a750 Scrub. 2015-12-30 12:38:01 -05:00
hacksalot
3e7d9c0411 Integrate JRSTheme class. 2015-12-30 12:37:26 -05:00
hacksalot
b21fd93d66 Introduce JRSTheme class.
Start splitting out logic into dedicated abstractions for both FRESH and
JSON Resume themes given the different structure and use cases of each.
2015-12-30 12:08:46 -05:00
hacksalot
37e75acd86 Merge remote-tracking branch 'refs/remotes/origin/master' into dev 2015-12-30 12:06:02 -05:00
Ya Zhuang
6280a18c14 fix: missing extend method
fix #68
2015-12-30 19:20:22 +08:00
hacksalot
5bc8b9c987 Merge remote-tracking branch 'refs/remotes/origin/dev' 2015-12-29 17:58:41 -05:00
hacksalot
0c570f8512 Update README. 2015-12-29 17:43:27 -05:00
hacksalot
7593afa586 Adjust package.json versions.
Relax to v1.3.0-beta and bump fluent-themes version.
2015-12-29 17:33:16 -05:00
hacksalot
417d07f469 Merge pull request #60 from zhuangya/images
Thanks @zhuangya! Nice work.
2015-12-29 17:21:37 -05:00
hacksalot
b803eba934 Scrub string.js.
Will probably be retired in favor of Node reusables.
2015-12-29 10:26:30 -05:00
hacksalot
483207e5a0 Improve Markdown support for JSON Resume themes. 2015-12-29 10:01:45 -05:00
hacksalot
02ef2b2241 Improve error handling.
Better support for spawn errors encountered during generation (for ex,
PDFs through wkhtml) + general refactoring.
2015-12-29 06:35:55 -05:00
hacksalot
13430bcad5 Refactor status codes. 2015-12-29 05:09:05 -05:00
hacksalot
e65c0e128e Fix tests glitch. 2015-12-29 03:50:00 -05:00
hacksalot
bf5c040971 Copy JRS theme assets to target. 2015-12-29 03:10:26 -05:00
Ya Zhuang
5dd3d1a3b4 chore: remove debugging console logs 2015-12-29 03:40:42 +08:00
Ya Zhuang
6b0ea0c7bd add: png format 2015-12-29 03:29:13 +08:00
hacksalot
6bc6b3262e Add tests for FRESH/JRS cross-generation.
Ability to generate JSON Resume themes from FRESH format resumes and
vice-versa.
2015-12-28 04:39:03 -05:00
hacksalot
3c1ae4cbd1 Add baseline support for local generation of JSON Resume themes. 2015-12-28 04:37:42 -05:00
hacksalot
547b87afc6 LINT prior to running tests. 2015-12-28 04:17:48 -05:00
hacksalot
db31744c98 Adjust "npm test" command.
Fix issue with tests being run twice and run tests through Grunt for
LINTing and other pre/post processing.
2015-12-28 04:16:53 -05:00
hacksalot
9423a19842 Remove extraneous references to "tests" plural. 2015-12-28 04:01:30 -05:00
hacksalot
07b303e530 Bump version to 1.3.0. 2015-12-28 03:51:39 -05:00
hacksalot
ec51148374 Introduce interim contribution guidelines. 2015-12-27 00:08:45 -05:00
hacksalot
0514f7805c Add contributors to package.json.
Contributors first, author last.
2015-12-26 22:47:39 -05:00
hacksalot
dfa19899b0 Merge pull request #41 from zhuangya/npm-test
package.json test scripts and travis :)
2015-12-26 21:12:39 -05:00
Ya Zhuang
1265ecab9f chore: remove generated resume, more node ci
- node 0.11 0.12
- remove and ignore `test/sandbox` from git
2015-12-26 19:13:15 +08:00
Ya Zhuang
1ad297ec7a add: missing travis-image url 2015-12-25 07:08:17 +08:00
Ya Zhuang
68628e3304 add: travis yml and badge 2015-12-25 07:06:52 +08:00
Ya Zhuang
1a6d7d5723 change: package.json enhancement
- add `scripts` for something like `npm test` and `npm run grunt`
- add missing devDep: `grunt-cli`
2015-12-25 07:03:16 +08:00
hacksalot
78a8b9c58e Merge pull request #40 from hacksalot/rel/v1.2.2
rel/v1.2.2
2015-12-24 17:44:56 -05:00
hacksalot
5e7abb66bd Safer source format conversions.
Quick fix against missing fields in FRESH and/or JRS (ahead of introing
more robust standalone converter thing). Address portions of #31 and
#33.
2015-12-24 17:51:26 -05:00
hacksalot
358c397bb9 Show call stack on error.
Hat tip @Furchin.
2015-12-24 16:22:29 -05:00
hacksalot
3d41528059 Fix path parsing issue on prev versions of Node.js.
Work around absence of path.parse in Node versions < v0.12. Addresses
#31 and #33.
2015-12-24 16:18:38 -05:00
hacksalot
79637b611a Bump version to 1.2.2. 2015-12-24 16:09:37 -05:00
hacksalot
5de796b119 Merge pull request #35 from hacksalot/rel/v1.2.1
rel/v1.2.1
2015-12-24 07:41:12 -05:00
hacksalot
bf84341acf Version -> 1.2.1
Holding off on 1.3.0 pending HTTP/HTTPS support. #rebase #semver
2015-12-24 07:42:07 -05:00
hacksalot
bbac1fdceb Improve test coverage around incomplete JRS resumes.
Add quick sanity checks around incomplete or irregular JRS-format
resumes. Also decorate existing JRS test resumes (/tests/resumes/) with
current 0.0.0 JRS version ahead of 1.0.0 release.
2015-12-24 06:08:45 -05:00
hacksalot
c5ee1ee33c Quick fix for ".history" errors.
Affects #31 and #33.
2015-12-24 04:05:56 -05:00
hacksalot
c74eda90ed Introduce lodash dependency. 2015-12-24 04:05:17 -05:00
hacksalot
ef2fe95bd8 Remove unused method. 2015-12-24 04:04:44 -05:00
hacksalot
e2589b3730 Fix validate command error.
Still hitting some inconsistent behavior in different NPM
versions/platforms with invalid uppercase dependency names per
https://github.com/npm/npm/issues/3692. Partial fix for #33.
2015-12-24 03:23:56 -05:00
hacksalot
ebad1677bc Replace file-exists.js with NPM path-exists. 2015-12-22 18:55:17 -05:00
hacksalot
dab6ebfd82 Bump fluent-themes version to 0.8.0-beta. 2015-12-22 18:49:29 -05:00
hacksalot
dd61b5360a Update package.json contact info. 2015-12-22 18:49:11 -05:00
hacksalot
fced92a5a0 Bump version to 1.3.0. 2015-12-22 18:47:23 -05:00
hacksalot
64db1a654e Merge pull request #30 from hacksalot/rel/v1.2.0
rel/v1.2.0
2015-12-21 02:57:59 -05:00
hacksalot
31830ee759 Silence tests. 2015-12-21 03:04:40 -05:00
hacksalot
1c05846a4f Add CLI tests. 2015-12-21 02:58:16 -05:00
hacksalot
1db9c2e420 Fix README glitch. 2015-12-21 02:58:01 -05:00
hacksalot
c966f6766c Refactor verbs to separate files. 2015-12-21 02:56:02 -05:00
hacksalot
65b6359fd8 Bump version to 1.2.0. 2015-12-21 00:52:23 -05:00
hacksalot
a54476eede Reaffirm string-based generation.
In recent commits, HackMyResume generation logic, much like the pilots
in Top Gun who became too reliant on air-to-air missiles and lost the
true art of dogfighting, has become dependent on file-based generation
as implicit file assumptions have crept in. This commit reaffirms the
file-less, string-based nature of the generation process and, as a side
effect, adjusts the behavior of (binary) PDF generation to match.
2015-12-21 00:36:08 -05:00
hacksalot
7c0354046c Merge pull request #29 from hacksalot/final/v1.1.0
final/v1.1.0
2015-12-20 20:46:19 -05:00
hacksalot
43cd1c7e52 Allow TO keyword to be omitted.
If the TO keyword is missing, assume the last file passed in is the
destination file.
2015-12-20 20:53:21 -05:00
hacksalot
f80c333361 Scrub. 2015-12-20 20:22:46 -05:00
hacksalot
cdbb264093 Add string convenience method. 2015-12-20 18:42:02 -05:00
hacksalot
87b3bbe785 Bump version to 1.1.0. 2015-12-20 18:41:51 -05:00
hacksalot
b92cf7298a Refactor helpers.
Helpers shouldn't be specific to a given template engine (eg,
Handlebars) in order to allow sharing of helpers between different
template engines. Isolate abstract helpers in another module and apply
them via Handlebars.registerHelper and as necessary for other template
engines.
2015-12-20 18:24:28 -05:00
hacksalot
93456b5f40 Merge pull request #28 from hacksalot/rel-next
rel-next
2015-12-20 15:56:43 -05:00
gruebait
72f29bf402 Update README. 2015-12-20 15:59:30 -05:00
gruebait
f6fc384466 Add keywords to package.json. 2015-12-20 15:43:31 -05:00
gruebait
c5ab3fdfae Bump version to 1.0.1. 2015-12-20 15:43:20 -05:00
gruebait
78c5081a29 Support Markdown-driven hyperlinks in MS Word. 2015-12-20 15:42:31 -05:00
hacksalot
d0c181ee8c Merge pull request #26 from hacksalot/rel-v1.0.0
v1.0.0
2015-12-19 12:33:19 -05:00
hacksalot
80c6bb6e8b Rename to HMR. 2015-12-19 12:37:42 -05:00
hacksalot
786b3fd3b2 Merge pull request #25 from fluentdesk/v0.11.0
v0.11.0
2015-12-19 08:58:59 -05:00
hacksalot
f0a22be731 Skip underscore-prefixed folders during theme load. 2015-12-19 08:39:36 -05:00
hacksalot
ade60022fd Introduce new helpers. 2015-12-19 00:24:21 -05:00
hacksalot
7daba910ed Bump fluent-themes version. 2015-12-18 17:27:40 -05:00
hacksalot
a016d6d91a Update README. 2015-12-18 17:23:40 -05:00
hacksalot
fcaa97ed35 Change author info. 2015-12-18 15:35:00 -05:00
hacksalot
bb7373a229 Remove duplicate is-my-json-valid dependency. 2015-12-18 15:34:52 -05:00
hacksalot
759dcc30e7 Update tests. 2015-12-18 15:34:30 -05:00
hacksalot
0e47f02a33 Rename tests/jane-doe.json. 2015-12-18 15:34:00 -05:00
hacksalot
5fe90517e7 Auto-clean tests folder. 2015-12-18 15:33:18 -05:00
hacksalot
92128da381 Install URL-trimming helper. 2015-12-18 14:51:51 -05:00
hacksalot
1441fe3ae5 Class-ify Underscore/Handlebars engine. 2015-12-18 13:17:07 -05:00
hacksalot
b0bc71cd66 Introduce "either" helper for Handlebars themes. 2015-12-18 13:00:47 -05:00
hacksalot
e908e8bb34 Add missing Underscore require(). 2015-12-18 10:13:50 -05:00
hacksalot
d708a6c6d8 Refactor Handlebars helpers. 2015-12-18 10:10:30 -05:00
hacksalot
a630741098 Fix exception. 2015-12-18 10:10:19 -05:00
hacksalot
01d148e47c Bump version to 0.11.0. 2015-12-18 10:08:52 -05:00
hacksalot
dbd41ec439 Bump FRESCA version. 2015-12-17 14:28:24 -05:00
devlinjd
fc9cbab974 Choose template engine from active theme. 2015-12-17 11:04:29 -05:00
hacksalot
36f8010ebc Merge pull request #24 from fluentdesk/feat-notices
feat-notices
2015-12-17 10:27:58 -05:00
devlinjd
e80d8fb5c8 Bump version. 2015-12-17 10:29:22 -05:00
devlinjd
eabab26eef Update file headers. 2015-12-17 10:15:59 -05:00
hacksalot
18dbb23168 Merge pull request #23 from fluentdesk/v0.10.2
v0.10.2
2015-12-17 07:52:21 -05:00
devlinjd
9ad2a1e92e Add simple-html-tokenizer dependency. 2015-12-16 23:28:57 -05:00
devlinjd
5475b081b1 Support basic Markdown in MS Word docs. 2015-12-16 23:26:53 -05:00
devlinjd
ae9c295ce1 Better Handlebars support. 2015-12-16 20:13:27 -05:00
devlinjd
e0ef774692 Add missing semicolon. 2015-12-16 12:13:50 -05:00
devlinjd
0c1364593a Support post-save callback. 2015-12-16 11:26:30 -05:00
devlinjd
1603a4bc73 Prosecute FCVD updates. 2015-12-16 11:25:50 -05:00
devlinjd
8361cf9960 Remove extraneous comma.
Go away, extraneous comma.
2015-12-16 11:25:04 -05:00
devlinjd
8273e7d150 ... 2015-12-15 06:20:06 -05:00
devlinjd
5c49a8297f Fix: FCVD error. 2015-12-14 07:32:41 -05:00
devlinjd
ee1e4bf699 Scrub. 2015-12-12 11:13:47 -05:00
devlinjd
f5a8e36e50 Refactor theme tests. 2015-12-12 11:13:37 -05:00
devlinjd
b38a7c1da2 Improve conversions and tests. 2015-12-12 10:48:26 -05:00
devlinjd
fe2247329e Bump version to 0.10.2. 2015-12-12 04:43:29 -05:00
devlinjd
9d459370ce Update FRESH<-->JRS converter. 2015-12-12 04:42:56 -05:00
hacksalot
201d96fe22 Merge pull request #22 from fluentdesk/v0.10.1
v0.10.1
2015-12-11 22:29:58 -05:00
devlinjd
8747429bc6 Update NPM registry description. 2015-12-11 22:35:26 -05:00
devlinjd
95540efe29 Tweak FRESCA and theme dependency versions. 2015-12-11 04:02:05 -05:00
devlinjd
0474dc7dbe Bump version to 0.10.1. 2015-12-11 03:04:34 -05:00
devlinjd
e5af6c38e0 Merge remote-tracking branch 'refs/remotes/origin/master' into v0.10.1 2015-12-11 03:01:31 -05:00
devlinjd
00e6407347 Kludge theme loading issue for FCVD.
Resolve in v0.11.0.
2015-12-10 10:28:19 -05:00
devlinjd
3805a36271 Fix folder generation wrinkle. 2015-12-09 23:30:53 -05:00
Sir Hacksalot
a3cefa1c42 Merge pull request #20 from fluentdesk/v0.10.0
v0.10.0
2015-12-09 23:28:25 -05:00
devlinjd
81276cf2cc Update README. 2015-12-09 23:09:33 -05:00
devlinjd
541198321e Fix JSHint warnings. 2015-12-09 21:44:35 -05:00
devlinjd
91aba39050 Add file LINTing through JSHint. 2015-12-09 21:44:18 -05:00
devlinjd
f7a3da0a4d Add generator tests for all themes. 2015-12-09 05:41:04 -05:00
devlinjd
0395792359 Restore canonical output filename. 2015-12-09 05:08:10 -05:00
devlinjd
2abfe4426c Refactor. 2015-12-09 04:32:48 -05:00
devlinjd
3dcf3c3974 Tweak Markdownification. 2015-12-09 04:32:39 -05:00
devlinjd
857de65750 More MEGADESK. 2015-12-09 00:13:58 -05:00
devlinjd
f3c9f92263 Add baseline Markdownification. 2015-12-08 22:22:33 -05:00
devlinjd
e8704e1374 Fix file generation glitch. 2015-12-08 22:22:14 -05:00
devlinjd
87c03b437c Generate safe date times; don't hard-code. 2015-12-08 22:21:42 -05:00
devlinjd
1a757e8a87 Bump FRESCA version to 0.2.0. 2015-12-08 21:12:19 -05:00
devlinjd
7c58f0ea96 Add symlink support. 2015-12-08 10:13:04 -05:00
devlinjd
fcaeb381fe Gather. 2015-12-07 21:24:14 -05:00
devlinjd
5a716dff16 Add basic multiplexing support. 2015-12-07 16:39:59 -05:00
devlinjd
8ee2716245 Scrub theme.js. 2015-12-07 10:16:38 -05:00
devlinjd
5f19f0a7df Add baseline support for multifile themes. #rough 2015-12-07 09:51:00 -05:00
devlinjd
cf25621679 Introduce placeholder LaTeX generator. 2015-12-06 18:29:16 -05:00
devlinjd
228f14d06c Support recursive theme template loading. 2015-12-06 18:19:33 -05:00
devlinjd
307c37dc44 Use "src" subfolder instead of "templates". 2015-12-06 18:18:36 -05:00
devlinjd
3b8d100f39 Add baseline Handlebars support. 2015-12-06 16:19:55 -05:00
devlinjd
fb783cdbc6 Add Handlebars dependency. 2015-12-06 14:57:20 -05:00
devlinjd
e4d098a3ce Add safety for implicit Markdown. 2015-12-06 05:51:03 -05:00
devlinjd
263f224e1b Bump fluent-themes version to 0.6.0-beta. 2015-12-06 05:50:14 -05:00
devlinjd
92ca11f23c Adjust output. 2015-12-02 15:10:38 -05:00
devlinjd
5b3a25c461 Support NEW command. 2015-12-02 14:56:36 -05:00
devlinjd
2431ae4d89 Bump version to 0.10.0. 2015-12-02 14:56:06 -05:00
devlinjd
2a8f0196b4 Update LICENSE author. 2015-12-01 14:33:49 -05:00
JD
d2791014ef Merge pull request #19 from fluentdesk/rel/v0.9.1
Rel/v0.9.1
2015-11-24 10:52:20 -05:00
devlinjd
e51eb270fc Bump versions. 2015-11-24 10:57:04 -05:00
devlinjd
fbc98060ce Fix theme loading glitch. 2015-11-24 10:56:28 -05:00
devlinjd
af33b6eded Merge branch 'refs/heads/feat/fresh' 2015-11-22 03:19:15 -05:00
devlinjd
51a44ce4a8 Merge remote-tracking branch 'refs/remotes/origin/master' into feat/fresh 2015-11-22 03:19:01 -05:00
JD
80315f12ac Merge pull request #18 from fluentdesk/feat/fresh
Feat/fresh
2015-11-22 03:09:31 -05:00
devlinjd
37225aec84 Update FRESCA version. 2015-11-22 03:03:16 -05:00
devlinjd
39fd689f61 Fix README table glitch. 2015-11-22 02:41:21 -05:00
devlinjd
5899989feb Update README. 2015-11-22 02:37:14 -05:00
devlinjd
42770989bc Tweak colors for Linux. 2015-11-22 00:10:08 -05:00
devlinjd
eade6f3a5c Tweak colors. 2015-11-22 00:07:30 -05:00
devlinjd
0fe334f433 Bump fluent-themes version. 2015-11-21 23:28:33 -05:00
devlinjd
5735ddc495 Multiple enhancements.
A set of rough enhancements supporting FRESH:

- Added ability to process multiple sources for all commands (BUILD,
VALIDATE, CONVERT).

- Added new HELP command to show usage.

- Improved error-handling and color-coding.
2015-11-21 16:12:22 -05:00
devlinjd
992069b22d Cleanup. 2015-11-21 10:33:16 -05:00
devlinjd
cbddb4b3aa Add convenience filter for links. 2015-11-21 09:13:21 -05:00
devlinjd
317de75a5b Refactor. 2015-11-21 07:59:30 -05:00
devlinjd
9fbab27d73 Improve validation and color-coding. 2015-11-21 05:56:16 -05:00
devlinjd
e44239b24a Update package.json deps. 2015-11-21 03:11:37 -05:00
devlinjd
debd866545 Adjust date references. 2015-11-21 03:11:18 -05:00
devlinjd
bf34b01367 Add YUIDoc support 2015-11-21 03:10:11 -05:00
devlinjd
5304cbabd9 Tweak converter. 2015-11-20 15:29:38 -05:00
devlinjd
4de997840e Scrub. 2015-11-20 09:53:36 -05:00
devlinjd
9cde39703e Clean up handling of "meta". 2015-11-20 09:28:55 -05:00
devlinjd
ad6d2c75ca Update FRESH tests. 2015-11-20 08:29:28 -05:00
devlinjd
c14176a504 Implement "convert" command. 2015-11-20 08:29:19 -05:00
devlinjd
16cf97e08e Improve converter. 2015-11-20 08:27:39 -05:00
devlinjd
c96d37b7f1 Update README. 2015-11-19 17:43:54 -05:00
devlinjd
15a74587bc Update FRESH tests with new exemplar name. 2015-11-19 17:19:27 -05:00
devlinjd
1b3fdfbc5f Add FRESH validation test. 2015-11-19 16:24:56 -05:00
devlinjd
0c1b1734ee Update tests. 2015-11-19 15:39:26 -05:00
devlinjd
35b9f2b764 Fix JSON date validation.
JSON "date" type should accept YYYY, YYYY-MM, and YYYY-MM-DD but
is-my-json-valid only validates the last of the three.
2015-11-19 12:36:58 -05:00
devlinjd
87618afa8d Remove unused verb. 2015-11-19 10:39:59 -05:00
devlinjd
458c8519b5 Update FRESH tests with recent changes. 2015-11-19 10:39:48 -05:00
devlinjd
0aa9bc2937 Rename Sheet/FreshSheet to JRSResume/FRESHResume. 2015-11-19 10:39:14 -05:00
devlinjd
a410153253 Implement "generate" and "validate" verbs.
Start moving to a more familiar verb-based interface with "generate" and
"validate" commands. Use with "fluentcv generate" or "fluentcv
validate".
2015-11-19 09:46:02 -05:00
devlinjd
9044dff504 Introduce FRESH and JSONResume conversion routines. 2015-11-19 09:39:49 -05:00
devlinjd
b167abcb78 Bump version to 0.9.0. 2015-11-19 01:57:43 -05:00
devlinjd
ce95593031 Relax copyright notices. 2015-11-19 01:57:15 -05:00
devlinjd
30b6bc4d80 Remove invalid object model reference. 2015-11-19 01:47:23 -05:00
devlinjd
0bebd87bd6 Rename JSON Resume test sheet to test-jrs-sheet.js. 2015-11-18 23:44:38 -05:00
devlinjd
f3eb46a154 Add starter tests for FRESH sheet. 2015-11-18 23:44:16 -05:00
devlinjd
6ce2ae2391 Introduce FRESH sheet class.
Introduce the canonical FRESH sheet class based on the old HackMyResume
(HMR) sources. Prepare to replace JSON Resume-specific handling with
generic FRESH handling.
2015-11-18 23:42:09 -05:00
JD
8cb3e8849e Merge pull request #17 from fluentdesk/ver/0.8.0
ver/0.8.0
2015-11-18 04:56:27 -05:00
devlinjd
07a072f8d7 Bump fluent-themes version. 2015-11-18 04:56:49 -05:00
devlinjd
8a56c61d56 Fix HTML-based PDF generator glitch. 2015-11-07 02:46:12 -05:00
devlinjd
99722e3bd1 Add filename metadata on save. 2015-11-06 14:56:33 -05:00
devlinjd
aa0ef4e8a4 Adjust CSS file handling by generators. 2015-11-05 00:57:57 -05:00
devlinjd
537bd4a7b9 Clear profiles on sheet reset. 2015-11-05 00:57:23 -05:00
devlinjd
9bd41d5825 Don't save "display_progress_bar". 2015-11-05 00:57:10 -05:00
devlinjd
4cc3fd3a1f Comments. 2015-11-05 00:56:41 -05:00
devlinjd
4752c3040e Store theme folder reference. 2015-11-05 00:56:06 -05:00
devlinjd
61c7d6b8f9 Bump version to 0.8.0. 2015-11-05 00:54:23 -05:00
devlinjd
89b7ed4d5b Expose FluentDate at API level. 2015-11-05 00:53:48 -05:00
devlinjd
fdfdd970a7 Bump version. 2015-10-27 21:48:30 -04:00
JD
22bb3252cd Merge pull request #15 from fluentdesk/ver/0.8.0
Bump version.
2015-10-27 21:48:28 -04:00
JD
b4907dc1b9 Merge pull request #14 from fluentdesk/ver/0.8.0
ver/0.7.2
2015-10-27 21:37:35 -04:00
devlinjd
2c6436be5e Bump fluent-themes version. 2015-10-27 21:11:00 -04:00
devlinjd
1e44ce5e5e Fix: Allow "current" in addition to "present" and "now". 2015-10-27 21:09:54 -04:00
devlinjd
9de5069c20 Fix: Allow year-only dates ('YYYY'). 2015-10-27 21:07:45 -04:00
devlinjd
b0b2af8278 Fix glitch with prettyifying HTML links. 2015-10-27 20:59:21 -04:00
JD
f9c4a70ca4 Merge pull request #13 from fluentdesk/ver/0.7.1
Ver/0.7.1
2015-10-27 07:35:40 -04:00
devlinjd
1782d06b37 Bump version. 2015-10-27 07:39:06 -04:00
devlinjd
5dee90b8e3 Remove process.exit() call. 2015-10-27 07:37:24 -04:00
JD
090b8a271a Merge pull request #12 from fluentdesk/feat/rename
Rename "FluentCMD" to "FluentCV".
2015-10-27 03:51:22 -04:00
devlinjd
330866a518 Rename "FluentCMD" to "FluentCV". 2015-10-27 03:54:50 -04:00
JD
783525c21d Merge pull request #9 from fluentdesk/rfact/fluent-themes
Rename watermark to fluent-themes.
2015-10-26 13:45:39 -04:00
devlinjd
6d6f66bfe2 Rename watermark to fluent-themes. 2015-10-26 13:48:00 -04:00
JD
6f578f9d44 Merge pull request #8 from fluentdesk/fix/title
Always display title on error.
2015-10-26 13:14:32 -04:00
devlinjd
e34d02facb Always display title on error. 2015-10-26 13:17:58 -04:00
JD
0d0b8a9d0b Merge pull request #7 from fluentdesk/rfact/libmerge
rfact/libmerge
2015-10-26 12:56:11 -04:00
devlinjd
5f50485968 Expose API surface. 2015-10-26 12:54:27 -04:00
devlinjd
2fdb1ac36c Tweak metadata. 2015-10-26 12:54:15 -04:00
devlinjd
0aaa9ffff8 Introduce FluentLib sources. 2015-10-26 12:30:00 -04:00
devlinjd
21d9db2d99 Update README. 2015-10-26 11:27:13 -04:00
119 changed files with 19096 additions and 391 deletions

17
.eslintrc.yml Normal file
View File

@ -0,0 +1,17 @@
env:
es6: true
node: true
extends: 'eslint:recommended'
rules:
# indent:
# - error
# - 4
linebreak-style:
- error
- unix
quotes:
- error
- single
semi:
- error
- always

6
.gitattributes vendored
View File

@ -1,9 +1,11 @@
# Auto detect text files and perform LF normalization
* text=auto
*.js text eol=lf
*.json text eol=lf
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
@ -14,3 +16,7 @@
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
# Git LFS
*.ai filter=lfs diff=lfs merge=lfs -text

39
.gitignore vendored
View File

@ -1 +1,40 @@
node_modules/
doc/
docs/
local/
npm-debug.log
*.map
# Emacs detritus
# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*
# Org-mode
.org-id-locations
*_archive
# flymake-mode
*_flymake.*
# eshell files
/eshell/history
/eshell/lastdir
# elpa packages
/elpa/
# reftex files
*.rel
# AUCTeX auto folder
/auto/
# cask packages
.cask/

13
.npmignore Normal file
View File

@ -0,0 +1,13 @@
assets/
test/
doc/
.travis.yml
.eslintrc.yml
Gruntfile.js
.gitattributes
ROADMAP.md
BUILDING.md
CONTRIBUTING.md
CHANGELOG.md
FAQ.md
*.map

23
.travis.yml Normal file
View File

@ -0,0 +1,23 @@
sudo: required
before_install:
# Prevents a shared object .so error when running wkhtmltopdf on certain
# platforms (e.g., vanilla Ubuntu 16.04 LTS). Not necessary on current Travis.
# - sudo apt-get install libxrender1
install:
# Install & link HackMyResume
- npm install && npm link
# Download and extract the latest wkhtmltopdf binaries
- 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
- tar -xf tmp/wk.tar.xz -C ./tmp
# Copy wkhtmltopdf binaries to /usr/bin (also makes them path-accessible)
- sudo cp -R ./tmp/wkhtmltox/bin/* /usr/bin/
# Now you can invoke "wkhtmltopdf" and "wkhtmltoimage" safely in tests.
- wkhtmltopdf -V
- wkhtmltoimage -V
language: node_js
node_js:
- "6"
- "7"
- "8"
- "9"
- "lts/*"

50
BUILDING.md Normal file
View File

@ -0,0 +1,50 @@
Building
========
*See [CONTRIBUTING.md][contrib] for more information on contributing to the
HackMyResume or FluentCV projects.*
HackMyResume is a standard Node.js command line app implemented in a mix of
CoffeeScript and JavaScript. Setting up a build environment is easy:
## Prerequisites ##
1. OS: Linux, OS X, or Windows
2. Install [Node.js][node] and [Grunt][grunt].
## Set up a build environment ###
1. Fork [hacksalot/HackMyResume][hmr] to your GitHub account.
2. Clone your fork locally.
3. From within the top-level HackMyResume folder, run `npm install` to install
project dependencies.
4. Create a new branch, based on the latest HackMyResume `dev` branch, to
contain your work.
5. Run `npm link` in the HackMyResume folder so that the `hackmyresume` command
will reference your local installation (you may need to
`npm uninstall -g hackmyresume` first).
## Making changes
1. HackMyResume sources live in the [`/src`][src] folder.
2. When you're ready to submit your changes, run `grunt test` to run the HMR
test suite. Fix any errors that occur.
3. Commit and push your changes.
4. Submit a pull request targeting the HackMyResume `dev` branch.
[node]: https://nodejs.org/en/
[grunt]: http://gruntjs.com/
[hmr]: https://github.com/hacksalot/HackMyResume
[src]: https://github.com/hacksalot/HackMyResume/tree/master/src
[contrib]: https://github.com/hacksalot/HackMyResume/blob/master/CONTRIBUTING.md

503
CHANGELOG.md Normal file
View File

@ -0,0 +1,503 @@
CHANGELOG
=========
## v1.9.0-beta
*Welcome to the first new version of HackMyResume in over a year. The purpose of
this release is to gather feature enhancements and bug fixes collected over the
past 18 months as we reorganize, rebrand, and prepare for the 2.0 release.*
### Added
- Support for **private resume fields**. Mark any non-leaf node in your resume
JSON with the `private` property and it will be omitted from outbound resumes.
```json
"employment": {
"history": [
{
"employer": "Acme Real Estate"
},
{
"employer": "Area 51 Alien Research Laboratory",
"private": true
},
{
"employer": "H&R Block"
}
]
}
```
- Support for **PDF generation through WeasyPrint** in addition to the
existing support for wkhtmltopdf and PhantomJS.
- Theme authors can now develop and package **custom Handlebars theme helpers**
via the `helpers` key of the `theme.json` file (FRESH themes only) (#158).
- Help system has been updated with a `HELP` command and dedicated help pages
for each command.
- Theme authors can **relocate theme assets** with the `baseFolder` property in
the FRESH `theme.json`.
- HackMyResume will now **validate the options file** (if any) loaded with `-o`
or `--options` and warn the user if necessary.
- Ability to **disable Handlebars encoding/escaping** of resume fields with
`--no-escape`.
- Introduced the [fresh-test-themes][ftt] project as a repository for simple,
test-only resume themes in the FRESH format.
### Changed
- Dropped support for Node 4 and 5. HackMyResume officially runs on Node 6+.
- The FRESCA project has been renamed to [fresh-resume-schema][fresca]. FRESCA
is still the nickname.
- The HackMyResume web page has moved to https://fluentdesk.com/hackmyresume.
### Fixed
- Fixed an issue that would cause the `convert` command to detect the inbound
resume type (FRESH or JRS) incorrectly (#162).
- Fixed an issue where generating new JRS resumes would cause a `null` or
`undefined` error (#165).
- Fixed an issue preventing mixed-case themes from being loaded (#172).
- Fixed an issue requiring JSON Resume themes contain `json-resume-theme` in the
theme path (#173).
- Fixed an issue that would cause strange `@@@@` characters to appear in
generated resumes (#207, #168, #198).
- Fixed an issue that would cause resume generation to hang after a JSON Resume
themed resume was generated (#182).
- Fixed an issue that would cause nothing to be generated for Markdown (.md)
formats (#179).
- Fixed an issue that would prevent a JRS resume from being converted to FRESH
via the `convert` command (#180).
- Fixed an issue that would cause broken styling for JSON Resume themes (#155).
### Internal
- Tests: fixed resume duration tests (#181).
- Style: move to
## v1.8.0
### Added
- Updated `Awesome` theme to latest version of [Awesome-CV][acv].
- Introduced new theme helpers: `pad`, `date`.
### Fixed
- Fixed an issue where the `Awesome` theme wouldn't correctly generate LaTeX
outputs (#138).
- Emit a line number for syntax errors around embedded newlines in JSON strings
(#137).
- Fix several PDF / PNG generation errors (#132, others).
- Display a more helpful error message when attempting to generate a PDF or PNG
on a machine where PhantomJS and/or wkhtmltopdf are either not installed or
not path-accessible.
- Fixed an issue that would cause long-running PDF/PNG generation to fail in
certain environments.
- Fixed an issue involving an unhelpful spawn-related exception (#136).
### Internal
- JSHint will no longer gripe at the use of `== null` and `!= null` in
CoffeeScript transpilation.
- Introduced [template-friendly Awesome-CV fork][awefork] to isolate template
expansion logic & provide better durability for HackMyResume's `awesome` theme.
- Fixed a couple temporary regressions (#139, #140) on the dev branch.
- Additional tests.
- Minor breaking HackMyResume API changes.
## v1.7.4
### Added
- [Build instructions](https://github.com/hacksalot/HackMyResume/blob/master/BUILDING.md).
### Changed
- More precise date handling.
### Fixed
- Issue with incomplete PDF generation (#127).
- Issue with building JSON Resume themes (#128).
- Issue with generating `.json` output format by itself (#97).
## v1.7.3
### Fixed
- Issue with generated PDFs being chopped off and displaying a mysterious sequence of numbers of unknown and possibly alien origin (#127).
- Unsightly border on Modern:PDF.
- Modern|Positive:PDF formats now correctly reference their PDF-specific CSS files.
- `Incorrect helper use` warning in Positive:DOC.
## v1.7.2
### Changed
- Interim release supporting FluentCV Desktop.
### Internal
- Moved [HackMyCore](https://github.com/hacksalot/HackMyCore) dependency to
submodule.
## v1.7.1
### Changed
- Caffeinate. CoffeeScript now used throughout
[HackMyResume](https://github.com/hacksalot/HackMyResume) and
[HackMyCore](https://github.com/hacksalot/HackMyCore); generated JavaScript
lives in `/dist`.
### Fixed
- Issue with generating a single PDF with the `.pdf` extension (#99).
## v1.7.0
### Changed
- [Internal] Relocated HMR processing code to the
[HackMyCore](https://github.com/hacksalot/HackMyCore) project. Shouldn't affect
normal use.
## v1.6.0
### Major Improvements
- Better consistency and coverage for all FRESH resumes and themes ([#45][i45]).
- Initial support for overridable fonts in FRESH themes. Like a particular
theme, but want to change the typography? The specific fonts used by a theme
can now be overridden by the user. (FRESH themes only).
- New resume sections! Support for `projects` and `affiliation` resume sections
for technical and creative projects and memberships / clubs / associations,
respectively ([#92][i92]).
- New command! `PEEK` at any arbitrary field or entry on your `.json` resume.
### Added
- Improved handling of start and end dates on `employment`, `projects`,
`education`, and other sections with start/end dates.
- Support for an `.ignore` property on any FRESH or JSON Resume section or field.
Ignored properties will be treated by HackMyResume as if they weren't present.
- Emit extended status and error info with the `--debug` or `-d` switch.
- The `-o` or `--options` switch can now handle either the path to a **JSON
settings file** or **raw JSON/JavaScript**. Since the JSON double quote syntax
is a bit cumbersome from the command line, HackMyResume accepts regular
JavaScript object literal syntax:
hackmyresume build resume.json -o "{ theme: 'compact', silent: 'true' }"
- Ability to disable sorting of resume sections (employments, projects, etc.)
with the `--no-sort` option. HMR will respect the order of items as they appear
in your resume `.json` file.
- Improvements to the starter resume emitted by `hackmyresume new`.
- Theme Authoring: Annotated the HTML and MS Word (XML) formats of the Modern
theme for FRESH theme authors.
- Theme Authoring: Support for templatized CSS files in FRESH themes. CSS files
are now expanded via Handlebars or Underscore prior to copying to the
destination.
- Added CHANGELOG.md (this file).
### Changed
- Rewrote the HackMyResume man/help page.
- Minor incremental updates to the [FRESCA][fresca] schema.
- PDF generation now uses asynchronous `spawn()` which has better compatibility
with old or boutique versions of Node.js.
- Refactored colors in HackMyResume output. Errors will now display as red,
warnings as yellow, successful operations as green, and informational messages
as cyan.
- Theme messages and usage tips will no longer display during resume generation
by default. Use the `--tips` option to view them.
- The `--no-tips` option (default: false) has been replaced with the `--tips`
option, also defaulting to false.
- Removed the `hello-world` theme from the [prebuilt themes][themes] that ship
with HackMyResume. It can be installed separately from NPM:
```bash
npm install fresh-theme-hello-world
hackmyresume resume.json -t node_modules/fresh-theme-hello-world
```
- sd
### Fixed
- PDF generation issues on older versions of Node.
- Stack traces not being emitted correctly.
- Missing `speaking` section will now appear on generated resumes ([#101][i101]).
- Incomplete `education` details will now appear on generated resumes ([#65][i65]).
- Missing employment end date being interpreted as "employment ends today"
([#84][i84]).
- Merging multiple source resumes during `BUILD` sometimes fails.
- Document `--pdf` flag in README ([#111][i111]).
### Internal
- Logging messages have been moved out of core HackMyResume code ahead of
localization support.
- All HackMyResume console output is described in `msg.yml`.
- Relaxed pure JavaScript requirement. CoffeeScript will now start appearing
in HackMyResume and FluentCV sources!
- Additional tests.
## v1.5.2
### Fixed
- Tweak stack trace under `--debug`.
## v1.5.1
### Added
- Preliminary support for `-d` or `--debug` flag. Forces HackMyResume to emit a stack trace under error conditions.
## v1.5.0
### Added
- HackMyResume now supports **CLI-based generation of PDF formats across multiple engines (Phantom, wkhtmltopdf, etc)**. Instead of talking to these engines over a programmatic API, as in prior versions, HackMyResume 1.5+ speaks to them over the same command-line interface (CLI) you'd use if you were using these tools directly.
- HackMyResume will now (attempt to) **generate a PDF output for JSON Resume themes** (in addition to HTML).
- Minor README and FAQ additions.
### Changed
- **Cleaner, quicker installs**. Installing HackMyResume with `npm install hackmyresume -g` will no longer trigger a lengthy, potentially error-prone install for Phantom.js and/or wkhtmltopdf for PDF support. Instead, users can install these engines externally and HMR will use them when present.
- Minor error handling improvements.
### Fixed
- Fixed an error with generating specific formats with the `BUILD` command (#97).
- Fixed numerous latent/undocumented bugs and glitches.
## v1.4.2
### Added
- Introduced [FAQ](https://github.com/hacksalot/HackMyResume/blob/master/FAQ.md).
- Additional README notes.
## v1.4.1
### Added
- `hackmyresume new` now generates a [valid starter resume with sample data](https://github.com/fluentdesk/fresh-resume-starter).
### Fixed
- Fixed warning message when `hackmyresume new` is run without a filename.
## v1.4.0
### Added
- **"Projects" support**: FRESH resumes and themes can now store and display
open source, commercial, private, personal, and creative projects.
- **New command: ANALYZE**. Inspect your resume for gaps, keyword counts, and other metrics. (Experimental.)
- **Side-by-side PDF generation** with Phantom and wkhtmltopdf. Use the `--pdf` or `-p` flag to pick between `phantom` and `wkhtmltopdf` generation.
- **Disable PDF generation** with the `--pdf none` switch.
- **Inherit formats between themes**. Themes can now inherit formats (Word, HTML, .txt, etc.) from other themes. (FRESH themes only.)
- **Rename resume sections** to different languages or wordings.
- **Specify complex options via external file**. Use with the `-o` or `--opts` option.
- **Disable colors** with the `--no-color` flag.
- **Theme messages and usage tips** instructions will now appear in the default HackMyResume output for the `build` command. Run `hackmyresume build resume.json -t awesome` for an example. Turn off with the `--no-tips` flag.
- **Treat validation errors as warnings** with the `--assert` switch (VALIDATE command only).
### Fixed
- Fixed a minor glitch in the FRESCA schema.
- Fixed encoding issues in the `Highlights` section of certain resumes.
- Fix behavior of `-s` and `--silent` flags.
### Changed
- PDF generation now defaults to Phantom for all platforms, with `wkhtmltopdf`
accessible with `--pdf wkhtmltopdf`.
- Resumes are now validated, by default, prior to generation. This
behavior can be disabled with the `--novalidate` or `--force` switch.
- Syntax errors in source FRESH and JSON Resumes are now captured for all
commands.
- Minor updates to README.
- Most themes now inherit Markdown and Plain Text formats from the **Basis**
theme.
### Internal
- Switched from color to chalk.
- Command invocations now handled through commander.js.
- Improved FRESH theme infrastructure (more partials, more DRY).
## v1.3.1
### Added
- Add additional Travis badges.
### Fixed
- Fix extraneous console log output when generating a FRESH theme to MS Word.
- Fix Travis tests on `dev`.
## v1.3.0
### Added
- **Local generation of JSON Resume themes**. To use a JSON Resume theme, first install it with `npm install jsonresume-theme-[blah]` (most JSON Resume themes are on NPM). Then pass it into HackMyResume via the `-t` parameter:
`hackmyresume BUILD resume.json TO out/somefile.all -t node_modules/jsonresume-theme-classy`
- **Better Markdown support.** HackMyResume will start flowing basic Markdown styles to JSON Resume (HTML) themes. FRESH's existing Markdown support has also been improved.
- **.PNG output formats** will start appearing in themes that declare an HTML output.
- **Tweak CSS embedding / linking via the --css option** (`<style></style>` vs `<link>`). Only works for HTML (or HTML-driven) formats of FRESH themes. Use `--css=link` to link in CSS assets and `--css=embed` to embed the styles in the HTML document. For example `hackmyresume BUILD resume.json TO out/resume.all --css=link`.
- **Improved Handlebars/Underscore helper support** for FRESH themes. Handlebars themes can access helpers via `{{helperName}}`. Underscore themes can access helpers via the `h` object.
### Changed
- **Distinguish between validation errors and syntax errors** when validating a FRESH or JRS resume with `hackmyresume validate <blah>`.
- **Emit line and column info** for syntax errors during validation of FRESH and JRS resumes.
- **FRESH themes now embed CSS into HTML formats by default** so that the HTML resume output doesn't have an external CSS file dependency by default. Users can specify normal linked stylesheets by setting `--css=link`.
- **Renamed fluent-themes repo to fresh-themes** in keeping with the other parts of the project.
### Fixed
- Fix various encoding errors in MS Word outputs.
- Fix assorted FRESH-to-JRS and JRS-to-FRESH conversion glitches.
- Fix error when running HMR with no parameters.
- Other minor fixes.
## v1.3.0-beta
- Numerous changes supporting v1.3.0.
## v1.2.2
### Fixed
- Various in-passing fixes.
## v1.2.1
### Fixed
- Fix `require('FRESCA')` error.
- Fix `.history` and `.map` errors on loading incomplete or empty JRS resumes.
### Added
- Better test coverage of incomplete/empty resumes.
## v1.2.0
### Fixed
- Fixed the `new` command: Generate a new FRESH-format resume with `hackmyresume new resume.json` or a new JSON Resume with `hackmyresume new resume.json -f jrs`.
### Added
- Introduced CLI tests.
## v1.1.0
### Fixed
- MS Word formats: Fixed skill coloring/level bug.
### Changed
- Make the `TO` keyword optional. If no `TO` keyword is specified (for the `build` and `convert` commands), HMR will assume the last file passed in is the desired output file. So these are equivalent:
```shell
hackmyresume BUILD resume.json TO out/resume.all
hackmyresume BUILD resume.json out/resume.all
```
`TO` only needs to be included if you have multipled output files:
```shell
hackmyresume BUILD resume.json TO out1.doc out2.html out3.tex
```
## v1.0.1
### Fixed
- Correctly generate MS Word hyperlinks from Markdown source data.
## v1.0.0
- Initial public 1.0 release.
[i45]: https://github.com/hacksalot/HackMyResume/issues/45
[i65]: https://github.com/hacksalot/HackMyResume/issues/65
[i84]: https://github.com/hacksalot/HackMyResume/issues/84
[i92]: https://github.com/hacksalot/HackMyResume/issues/92
[i101]: https://github.com/hacksalot/HackMyResume/issues/101
[i111]: https://github.com/hacksalot/HackMyResume/issues/111
[fresca]: https://github.com/fresh-standard/fresh-resume-schema
[themes]: https://github.com/fresh-standard/fresh-themes
[awefork]: https://github.com/fluentdesk/Awesome-CV
[acv]: https://github.com/posquit0/Awesome-CV
[ftt]: https://github.com/fresh-standard/fresh-test-themes

53
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,53 @@
Contributing
============
*Note: HackMyResume is also available as [FluentCV][fcv]. Contributors are
credited in both.*
## How To Contribute
*See [BUILDING.md][building] for instructions on setting up a HackMyResume
development environment.*
1. Optional: [**open an issue**][iss] identifying the feature or bug you'd like
to implement or fix. This step isn't required — you can start hacking away on
HackMyResume without clearing it with us — but helps avoid duplication of work
and ensures that your changes will be accepted once submitted.
2. **Fork and clone** the HackMyResume project.
3. Ideally, **create a new feature branch** (eg, `feat/new-awesome-feature` or
similar; call it whatever you like) to perform your work in.
4. **Install dependencies** by running `npm install` in the top-level
HackMyResume folder.
5. Make your **commits** as usual.
6. **Verify** your changes locally with `grunt test`.
7. **Push** your commits.
7. **Submit a pull request** from your feature branch to the HackMyResume `dev`
branch.
8. We'll typically **respond** within 24 hours.
9. Your awesome changes will be **merged** after verification.
## Project Maintainers
HackMyResume is currently maintained by [hacksalot][ha] with assistance from
[tomheon][th] and our awesome [contributors][awesome]. Please direct all official
or internal inquiries to:
```
admin@fluentdesk.com
```
You can reach hacksalot directly at:
```
hacksalot@indevious.com
```
Thanks for your interest in the HackMyResume project.
[fcv]: https://github.com/fluentdesk/fluentcv
[flow]: https://guides.github.com/introduction/flow/
[iss]: https://github.com/hacksalot/HackMyResume/issues
[ha]: https://github.com/hacksalot
[th]: https://github.com/tomheon
[awesome]: https://github.com/hacksalot/HackMyResume/graphs/contributors
[building]: https://github.com/hacksalot/HackMyResume/blob/master/BUILDING.md

228
FAQ.md Normal file
View File

@ -0,0 +1,228 @@
Frequently Asked Questions (FAQ)
================================
## How do I get started with HackMyResume?
1. Install with NPM: `[sudo] npm install hackmyresume -g`.
2. Create a new resume with: `hackmyresume NEW <resume-name>.json`.
3. Test with `hackmyresume BUILD <resume-name>.json`. Look in the `out/` folder.
4. Play around with different themes with the `-t` or `--theme` parameter.
You can use any [FRESH](https://github.com/fluentdesk/fresh-themes) or
[JSON Resume](https://jsonresume.org/themes) theme. The latter have to be
installed first.
## What is FRESH?
FRESH is the **F**luent **R**esume and **E**mployment **S**ystem for **H**umans.
It's an open-source, user-first workflow, schema, and set of practices for
technical candidates and recruiters.
## What is FRESCA?
The **F**RESH **R**esume and **E**mployment **SC**hem**A**&mdash;an open-source,
JSON-driven schema for resumes, CVs, and other employment artifacts. FRESCA is
the recommended schema/format for FRESH, with optional support for JSON Resume.
## What is JSON Resume?
An [open resume standard](http://jsonresume.org/themes/) sponsored by Hired.com.
Like FRESCA, JSON Resume is JSON-driven and open-source. Unlike FRESCA, JSON
Resume targets a worldwide audience where FRESCA is optimized for technical
candidates.
## Should I use the FRESH or JSON Resume format/schema for my resume?
Both! The workflow we like to use:
1. Create a resume in FRESH format for tooling and analysis.
2. Convert it to JSON Resume format for additional themes/tools.
3. Maintain both versions.
Both formats are open-source and both formats are JSON-driven. FRESH was
designed as a universal container format and superset of existing formats, where
the JSON Resume format is intended for a generic audience.
## How do I use a FRESH theme?
Several FRESH themes come preinstalled with HackMyResume; others can be
installed from NPM and GitHub.
### To use a preinstalled FRESH theme:
1. Pass the theme name into HackMyResume via the `--theme` or `-t` parameter:
```bash
hackmyresume build resume.json --theme compact
```
### To use an external FRESH theme:
1. Install the theme locally. The easiest way to do that is with NPM.
```bash
npm install fresh-theme-underscore
```
2. Pass the theme folder into HackMyResume:
```bash
hackmyresume BUILD resume.json --theme node_modules/fresh-theme-underscore
```
3. Check your output folder. It's best to view HTML formats over a local web
server connection.
## How do I use a JSON Resume theme?
JSON Resume (JRS) themes can be installed from NPM and GitHub and passed into
HackMyResume via the `--theme` or `-t` parameter.
1. Install the theme locally. The easiest way to do that is with NPM.
```bash
npm install jsonresume-theme-classy
```
2. Pass the theme folder path into HackMyResume:
```bash
hackmyresume BUILD resume.json --theme node_modules/jsonresume-theme-classy
```
3. Check your output folder. It's best to view HTML formats over a local web
server connection.
## Should I keep my resume in version control?
Absolutely! As text-based, JSON-driven documents, both FRESH and JSON Resume are
ideal candidates for version control. Future versions of HackMyResume will have
this functionality built in.
## Can I change the default section titles ("Employment", "Skills", etc.)?
If you're using a FRESH theme, yes. First, create a HackMyResume options file
mapping resume sections to your preferred section title:
```javascript
// myoptions.json
{
"sectionTitles": {
"employment": "empleo",
"skills": "habilidades",
"education": "educación"
}
}
```
Then, pass the options file into the `-o` or `--opts` parameter:
```bash
hackmyresume BUILD resume.json -o myoptions.json
```
This ability is currently only supported for FRESH resume themes.
## How does resume merging work?
Resume merging is a way of storing your resume in separate files that
HackMyResume will merge into a single "master" resume file prior to generating
specific output formats like HTML or PDF. It's a way of producing flexible,
configurable, targeted resumes with minimal duplication.
For example, a software developer who moonlights as a game programmer might
create three FRESH or JRS resumes at different levels of specificity:
- **generic.json**: A generic technical resume, suitable for all audiences.
- **game-developer.json**: Overrides and amendments for game developer
positions.
- **blizzard.json**: Overrides and amendments specific to a hypothetical
position at Blizzard.
If you run `hackmyresume BUILD generic.json TO out/resume.all`, HMR will
generate all available output formats for the `generic.json` as usual. But if
you instead run...
```bash
hackmyresume BUILD generic.json game-developer.json TO out/resume.all
```
...HackMyResume will notice that multiple source resumes were specified and
merge `game-developer.json` onto `generic.json` before generating, yielding a
resume that's more suitable for game-developer-related positions.
You can take this a step further. Let's say you want to do a targeted resume
submission to a game developer position at Blizzard, and `blizzard.json`
contains the edits and revisions you'd like to show up in the targeted resume.
In that case, merge again! Feed all three resumes to HackMyResume, in order
from most generic to most specific, and HMR will merge them all prior to
generating the final output format(s) for your resume.
```bash
# Merge blizzard.json onto game-developer.json onto generic.json, then build
hackmyresume BUILD generic.json game-developer.json blizzard.json TO out/resume.all
```
There's no limit to the number of resumes you can merge this way.
You can also divide your resume into files containing different sections:
- **resume-a.json**: Contains `info`, `employment`, and `summary` sections.
- **resume-b.json**: Contains all other sections except `references`.
- **references.json**: Contains the private `references` section.
Under that scenario, `hackmyresume BUILD resume-a.json resume-b.json` would
## The HackMyResume terminal color scheme is giving me a headache. Can I disable it?
Yes. Use the `--no-color` option to disable terminal colors:
`hackmyresume <somecommand> <someoptions> --no-color`
## What's the difference between a FRESH theme and a JSON Resume theme?
FRESH themes are multiformat (HTML, Word, PDF, etc.) and required to support
Markdown formatting, configurable section titles, and various other features.
JSON Resume themes are typically HTML-driven, but capable of expansion to other
formats through tools. JSON Resume themes don't support Markdown natively, but
HMR does its best to apply your Markdown, when present, to any JSON Resume
themes it encounters.
## Do I have to have a FRESH resume to use a FRESH theme or a JSON Resume to use a JSON Resume theme?
No. You can mix and match FRESH and JRS-format themes freely. HackMyResume will
perform the necessary conversions on the fly.
## Can I build my own custom FRESH theme?
Yes. The easiest way is to copy an existing FRESH theme, like `modern` or
`compact`, and make your changes there. You can test your theme with:
```bash
hackmyresume build resume.json --theme path/to/my/theme/folder
```
## Can I build my own custom JSON Resume theme?
Yes. The easiest way is to copy an existing JSON Rsume theme and make your
changes there. You can test your theme with:
```bash
hackmyresume build resume.json --theme path/to/my/theme/folder
```
## Can I build my own tools / services / apps / websites around FRESH / FRESCA?
Yes! FRESH/FRESCA formats are 100% open source, permissively licensed under MIT,
and 100% free from company-specific, tool-specific, or commercially oriented
lock-in or cruft. These are clean formats designed for users and builders.
## Can I build my own tools / services / apps / websites around JSON Resume?
Yes! HackMyResume is not affiliated with JSON Resume, but like FRESH/FRESCA,
JSON Resume is open-source, permissively licensed, and free of proprietary
lock-in. See the JSON Resume website for details.

52
Gruntfile.js Normal file
View File

@ -0,0 +1,52 @@
module.exports = function (grunt) {
'use strict';
var opts = {
pkg: grunt.file.readJSON('package.json'),
simplemocha: {
options: {
globals: ['expect', 'should'],
timeout: 3000,
ignoreLeaks: false,
ui: 'bdd',
reporter: 'spec'
},
all: { src: ['test/*.js'] }
},
clean: {
test: ['test/sandbox']
},
eslint: {
target: ['Gruntfile.js', 'src/**/*.js', 'test/*.js']
}
};
grunt.initConfig( opts );
grunt.loadNpmTasks('grunt-simple-mocha');
grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('grunt-contrib-clean');
// Use 'grunt test' for local testing
grunt.registerTask('test', 'Test the HackMyResume application.',
function() {
grunt.task.run(['clean:test','build','eslint','simplemocha:all']);
}
);
// Use 'grunt build' to build HMR
grunt.registerTask('build', 'Build the HackMyResume application.',
function() {
grunt.task.run( ['eslint'] );
}
);
// Default task does everything
grunt.registerTask('default', [ 'test' ]);
};

View File

@ -1,7 +1,7 @@
The MIT License
===============
Copyright (c) 2015 James M. Devlin (https://github.com/devlinjd)
Copyright (c) 2015-2018 hacksalot (https://github.com/hacksalot)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

601
README.md
View File

@ -1,77 +1,219 @@
fluentcmd
=========
*Generate beautiful, targeted resumes from your command line or shell.*
HackMyResume
===
FluentCMD is a **hackable, data-driven, dev-friendly resume authoring tool** with support for HTML, Markdown, Word, PDF, plain text, smoke signal, carrier pigeon, and other arbitrary-format resumes and CVs.
[![Latest release][img-release]][latest-release]
[![Build status (MASTER)][img-master]][travis-url-master]
[![Build status (DEV)][img-dev]][travis-url-dev]
[![Join the chat at https://gitter.im/hacksalot/HackMyResume][badge]][gh]
```bash
>fluentcmd resume.json -o out/resume.all -t modern
*Create polished résumés and CVs in multiple formats from your command line or
shell. Author in clean Markdown and JSON, export to Word, HTML, PDF, LaTeX,
plain text, and other arbitrary formats. Fight the power, save trees. Compatible
with [FRESH][fresca] and [JRS][6] resumes.*
*** FluentCMD v0.5.0 ***
Reading JSON resume: foo/resume.json
Applying MODERN Theme (7 formats)
Generating HTML resume: out/resume.html
Generating TXT resume: out/resume.txt
Generating DOC resume: out/resume.doc
Generating PDF resume: out/resume.pdf
Generating JSON resume: out/resume.json
Generating MARKDOWN resume: out/resume.md
Generating YAML resume: out/resume.yml
```
![](assets/hmr_build.png)
Looking for a desktop GUI version with pretty timelines and graphs? Check out [FluentCV Desktop][7].
HackMyResume is a dev-friendly, local-only Swiss Army knife for resumes and CVs.
Use it to:
1. **Generate** HTML, Markdown, LaTeX, MS Word, PDF, plain text, JSON, XML,
YAML, print, smoke signal, carrier pigeon, and other arbitrary-format resumes
and CVs, from a single source of truth&mdash;without violating DRY.
2. **Analyze** your resume for keyword density, gaps/overlaps, and other
metrics.
3. **Convert** resumes between [FRESH][fresca] and [JSON Resume][6] formats.
4. **Validate** resumes against either format.
HackMyResume is built with Node.js and runs on recent versions of OS X, Linux,
or Windows. View the [FAQ](FAQ.md).
![](assets/hmr_analyze.png)
## Features
- Runs on OS X, Linux, and Windows.
- Store your resume data as a durable, versionable JSON, YML, or XML document.
- Generate multiple targeted resumes in multiple formats, based on your needs.
- Output to HTML, PDF, Markdown, Word, JSON, YAML, XML, or a custom format.
- Never update one piece of information in four different resumes again.
- Compatible with the [JSON Resume standard][6] and [authoring tools][7].
- OS X, Linux, and Windows.
- Choose from dozens of FRESH or JSON Resume themes.
- Private, local-only resume authoring and analysis.
- Analyze your resume for keywords, gaps, and other metrics.
- Store your resume data as a durable, versionable JSON or YAML document.
- Generate polished resumes in multiple formats without violating [DRY][dry].
- Output to HTML, Markdown, LaTeX, PDF, MS Word, JSON, YAML, plain text, or XML.
- Validate resumes against the FRESH or JSON Resume schema.
- Support for multiple input and output resumes.
- Convert between FRESH and JSON Resume resumes.
- Use from your command line or [desktop][7].
- Free and open-source through the MIT license.
- Forthcoming: StackOverflow and LinkedIn support.
- Forthcoming: More themes!
- Updated daily / weekly. Contributions are [welcome](CONTRIBUTING.md).
## Install
FluentCMD requires a recent version of [Node.js][4] and [NPM][5]. Then:
Install the latest stable version of HackMyResume with NPM:
1. (Optional, for PDF support) Install the latest official [wkhtmltopdf][3] binary for your platform.
2. Install **fluentcmd** by running `npm install fluentcmd -g`.
3. You're ready to go.
```bash
[sudo] npm install hackmyresume -g
```
Alternately, install the latest bleeding-edge version (updated daily):
```bash
[sudo] npm install hacksalot/hackmyresume#dev -g
```
## Installing PDF Support (optional)
HackMyResume tries not to impose a specific PDF engine requirement on
the user, but will instead work with whatever PDF engines you have installed.
Currently, HackMyResume's PDF generation requires one of [Phantom.js][2],
[wkhtmltopdf][3], or [WeasyPrint][11] to be installed on your system and the
corresponding binary to be accessible on your PATH. This is an optional
requirement for users who care about PDF formats. If you don't care about PDF
formats, skip this step.
## Installing Themes
HackMyResume supports both [FRESH][fresh-themes] and [JSON Resume][jrst]-style
résumé themes.
- FRESH themes currently come preinstalled with HackMyResume.
- JSON Resume themes can be installed from NPM, GitHub, or manually.
To install a JSON Resume theme, just `cd` to the folder where you want to store
your themes and run one of:
```bash
# Install with NPM
npm install jsonresume-theme-[theme-name]
# Install with GitHub
git clone https://github.com/[user-or-org]/[repo-name]
```
Then when you're ready to generate your resume, just reference the location of
the theme folder as you installed it:
```bash
hackmyresume build resume.json TO out/resume.all -t node_modules/jsonresume-theme-classy
```
Note: You can use install themes anywhere on your file system. You don't need a
package.json or other NPM/Node infrastructure.
## Getting Started
To use HackMyResume you'll need to create a valid resume in either
[FRESH][fresca] or [JSON Resume][6] format. Then you can start using the command
line tool. There are five basic commands you should be aware of:
- **build** generates resumes in HTML, Word, Markdown, PDF, and other formats.
Use it when you need to submit, upload, print, or email resumes in specific
formats.
```bash
# hackmyresume build <INPUTS...> TO <OUTPUTS...> [-t THEME]
hackmyresume build resume.json TO out/resume.all
hackmyresume build r1.json r2.json TO out/rez.html out/rez.md foo/rez.all
```
- **new** creates a new resume in FRESH or JSON Resume format.
```bash
# hackmyresume new <OUTPUTS...> [-f <FORMAT>]
hackmyresume new resume.json
hackmyresume new resume.json -f fresh
hackmyresume new r1.json r2.json -f jrs
```
- **analyze** inspects your resume for keywords, duration, and other metrics.
```bash
# hackmyresume analyze <INPUTS...>
hackmyresume analyze resume.json
hackmyresume analyze r1.json r2.json
```
- **convert** converts your source resume between FRESH and JSON Resume
formats. Use it to convert between the two formats to take advantage of tools
and services.
```bash
# hackmyresume convert <INPUTS...> TO <OUTPUTS...>
hackmyresume convert resume.json TO resume-jrs.json
hackmyresume convert 1.json 2.json 3.json TO out/1.json out/2.json out/3.json
```
- **validate** validates the specified resume against either the FRESH or JSON
Resume schema. Use it to make sure your resume data is sufficient and complete.
```bash
# hackmyresume validate <INPUTS...>
hackmyresume validate resume.json
hackmyresume validate r1.json r2.json r3.json
```
- **peek** echoes your resume or any field, property, or object path on your
resume to standard output.
```bash
# hackmyresume peek <INPUTS...> [OBJECT-PATH]
hackmyresume peek rez.json # Echo the whole resume
hackmyresume peek rez.json info.brief # Echo the "info.brief" field
hackmyresume peek rez.json employment.history[1] # Echo the 1st job
hackmyresume peek rez.json rez2.json info.brief # Compare value
```
## Supported Output Formats
HackMyResume supports these output formats:
Output Format | Ext | Notes
------------- | --- | -----
HTML | .html | A standard HTML 5 + CSS resume format that can be viewed in a browser, deployed to a website, etc.
Markdown | .md | A structured Markdown document that can be used as-is or used to generate HTML.
LaTeX | .tex | A structured LaTeX document (or collection of documents) that can be processed with pdflatex, xelatex, and similar tools.
MS Word | .doc | A Microsoft Word office document (XML-driven; WordProcessingML).
Adobe Acrobat (PDF) | .pdf | A binary PDF document driven by an HTML theme (through wkhtmltopdf).
plain text | .txt | A formatted plain text document appropriate for emails or copy-paste.
JSON | .json | A JSON representation of the resume.
YAML | .yml | A YAML representation of the resume.
RTF | .rtf | Forthcoming.
Textile | .textile | Forthcoming.
image | .png, .bmp | Forthcoming.
## Use
Assuming you've got a JSON-formatted resume handy, generating resumes in different formats and combinations easy. Just run:
Assuming you've got a JSON-formatted resume handy, generating resumes in
different formats and combinations is easy. Just run:
```bash
fluentcmd [inputs] [outputs] [-t theme].
hackmyresume build <inputs> to <outputs> [-t theme].
```
Where `[inputs]` is one or more .json resume files, separated by spaces; `[outputs]` is one or more destination resumes, each prefaced with the `-o` option; and `[theme]` is the desired theme. For example:
Where `<INPUTS>` is one or more .json resume files, separated by spaces;
`<OUTPUTS>` is one or more destination resumes, and `<THEME>` is the desired
theme (default to Modern). For example:
```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, YML, etc.)
fluentcmd resume.json -o out/resume.all -t modern
hackmyresume build resume.json TO out/resume.all -t modern
# Generate a specific resume format
fluentcmd resume.json -o out/resume.html
fluentcmd resume.json -o out/resume.pdf
fluentcmd resume.json -o out/resume.md
fluentcmd resume.json -o out/resume.doc
fluentcmd resume.json -o out/resume.json
fluentcmd resume.json -o out/resume.txt
fluentcmd resume.json -o out/resume.yml
hackmyresume build resume.json TO out/resume.html
hackmyresume build resume.json TO out/resume.pdf
hackmyresume build resume.json TO out/resume.md
hackmyresume build resume.json TO out/resume.doc
hackmyresume build resume.json TO out/resume.json
hackmyresume build resume.json TO out/resume.txt
hackmyresume build resume.json TO out/resume.yml
# Specify 2 inputs and 3 outputs
fluentcmd in1.json in2.json -o out.html -o out.doc -o out.pdf
hackmyresume build in1.json in2.json TO out.html out.doc out.pdf
```
You should see something to the effect of:
```
*** FluentCMD v0.5.0 ***
*** HackMyResume v1.4.0 ***
Reading JSON resume: foo/resume.json
Applying MODERN Theme (7 formats)
Generating HTML resume: out/resume.html
@ -87,28 +229,56 @@ Generating YAML resume: out/resume.yml
### Applying a theme
You can specify a predefined or custom theme via the optional `-t` parameter. For a predefined theme, include the theme name. For a custom theme, include the path to the custom theme's folder.
HackMyResume can work with any FRESH or JSON Resume theme (the latter must be
installed first). To specify a theme when generating your resume, use the `-t`
or `--theme` parameter:
```bash
fluentcmd resume.json -t modern
fluentcmd resume.json -t ~/foo/bar/my-custom-theme/
hackmyresume build resume.json TO out/rez.all -t [theme]
```
As of v0.5.0, available predefined themes are `modern`, `minimist`, and `hello-world`.
The `[theme]` parameter can be the name of a predefined theme OR the path to any
FRESH or JSON Resume theme folder:
```bash
hackmyresume build resume.json TO out/rez.all -t modern
hackmyresume build resume.json TO OUT.rez.all -t ../some-folder/my-custom-theme/
hackmyresume build resume.json TO OUT.rez.all -t node_modules/jsonresume-theme-classy
```
FRESH themes are currently pre-installed with HackMyResume. JSON Resume themes
can be installed prior to use:
```bash
# Install a JSON Resume theme into a local node_modules subfolder:
npm install jsonresume-theme-[name]
# Use it with HackMyResume
hackmyresume build resume.json -t node_modules/jsonresume-theme-[name]
```
As of v1.6.0, available predefined FRESH themes are `positive`, `modern`,
`compact`, `minimist`, and `hello-world`. For a list of JSON Resume themes,
check the [NPM Registry](https://www.npmjs.com/search?q=jsonresume-theme).
### Merging resumes
You can **merge multiple resumes together** by specifying them in order from most generic to most specific:
You can **merge multiple resumes together** by specifying them in order from
most generic to most specific:
```bash
# Merge specific.json onto base.json and generate all formats
fluentcmd base.json specific.json -o resume.all
hackmyresume build base.json specific.json TO resume.all
```
This can be useful for overriding a base (generic) resume with information from a specific (targeted) resume. For example, you might override your generic catch-all "software developer" resume with specific details from your targeted "game developer" resume, or combine two partial resumes into a "complete" resume. Merging follows conventional [extend()][9]-style behavior and there's no arbitrary limit to how many resumes you can merge:
This can be useful for overriding a base (generic) resume with information from
a specific (targeted) resume. For example, you might override your generic
catch-all "software developer" resume with specific details from your targeted
"game developer" resume, or combine two partial resumes into a "complete"
resume. Merging follows conventional [extend()][9]-style behavior and there's
no arbitrary limit to how many resumes you can merge:
```bash
fluentcmd in1.json in2.json in3.json in4.json -o out.html -o out.doc
hackmyresume build in1.json in2.json in3.json in4.json TO out.html out.doc
Reading JSON resume: in1.json
Reading JSON resume: in2.json
Reading JSON resume: in3.json
@ -120,37 +290,237 @@ Generating WORD resume: out.doc
### Multiple targets
You can specify **multiple output targets** and FluentCMD will build them:
You can specify **multiple output targets** and HackMyResume will build them:
```bash
# Generate out1.doc, out1.pdf, and foo.txt from me.json.
fluentcmd me.json -o out1.doc -o out1.pdf -o foo.txt
```
You can also omit the output file(s) and/or theme completely:
```bash
# Equivalent to "fluentcmd resume.json resume.all -t modern"
fluentcmd resume.json
hackmyresume build me.json TO out1.doc out1.pdf foo.txt
```
### Using .all
The special `.all` extension tells FluentCMD to generate all supported output formats for the given resume. For example, this...
The special `.all` extension tells HackMyResume to generate all supported output
formats for the given resume. For example, this...
```bash
# Generate all resume formats (HTML, PDF, DOC, TXT, etc.)
fluentcmd me.json -o out/resume.all
hackmyresume build me.json TO out/resume.all
```
..tells FluentCV to read `me.json` and generate `out/resume.md`, `out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and `out/resume.json`.
..tells HackMyResume to read `me.json` and generate `out/resume.md`,
`out/resume.doc`, `out/resume.html`, `out/resume.txt`, `out/resume.pdf`, and
`out/resume.json`.
### Building PDFs
*Users who don't care about PDFs can turn off PDF generation across all themes
and formats with the `--pdf none` switch.*
HackMyResume takes a unique approach to PDF generation. Instead of enforcing
a specific PDF engine on users, HackMyResume will attempt to work with whatever
PDF engine you have installed through the engine's command-line interface (CLI).
Currently that means any of...
- [wkhtmltopdf][3]
- [Phantom.js][2]
- [WeasyPrint][11]
..with support for other engines planned in the future. But for now, **one or
more of these engines must be installed and accessible on your PATH in order
to generate PDF resumes with HackMyResume**. That means you should be able to
invoke either of these tools directly from your shell or terminal without error:
```bash
wkhtmltopdf input.html output.pdf
phantomjs script.js input.html output.pdf
weasyprint input.html output.pdf
```
Assuming you've installed one or both of these engines on your system, you can
tell HackMyResume which flavor of PDF generation to use via the `--pdf` option
(`-p` for short):
```bash
hackmyresume build resume.json TO out.all --pdf phantom
hackmyresume build resume.json TO out.all --pdf wkhtmltopdf
hackmyresume build resume.json TO out.all --pdf weasyprint
hackmyresume build resume.json TO out.all --pdf none
```
### Analyzing
HackMyResume can analyze your resume for keywords, employment gaps, and other
metrics. Run:
```bash
hackmyresume analyze <my-resume>.json
```
Depending on the HackMyResume version, you should see output similar to:
```
*** HackMyResume v1.6.0 ***
Reading resume: resume.json
Analyzing FRESH resume: resume.json
SECTIONS (10):
employment: 12
education: 2
service: 1
skills: 8
writing: 1
recognition: 0
social: 4
interests: 2
references: 1
languages: 2
COVERAGE (61.1%):
Total Days: 6034
Employed: 3688
Gaps: 8 [31, 1065, 273, 153, 671, 61, 61, 31]
Overlaps: 1 [243]
KEYWORDS (61):
Node.js: 6 mentions
JavaScript: 9 mentions
SQL Server: 3 mentions
Visual Studio: 6 mentions
Web API: 1 mentions
N-tier / 3-tier: 1 mentions
HTML 5: 1 mentions
JavaScript: 6 mentions
CSS: 2 mentions
Sass / LESS / SCSS: 1 mentions
LAMP: 3 mentions
WISC: 1 mentions
HTTP: 21 mentions
JSON: 1 mentions
XML: 2 mentions
REST: 1 mentions
WebSockets: 2 mentions
Backbone.js: 3 mentions
Angular.js: 1 mentions
Node.js: 4 mentions
NPM: 1 mentions
Bower: 1 mentions
Grunt: 2 mentions
Gulp: 1 mentions
jQuery: 2 mentions
Bootstrap: 3 mentions
Underscore.js: 1 mentions
PhantomJS: 1 mentions
CoffeeScript: 1 mentions
Python: 11 mentions
Perl: 4 mentions
PHP: 7 mentions
MySQL: 12 mentions
PostgreSQL: 4 mentions
NoSQL: 2 mentions
Apache: 2 mentions
AWS: 2 mentions
EC2: 2 mentions
RDS: 3 mentions
S3: 1 mentions
Azure: 1 mentions
Rackspace: 1 mentions
C++: 23 mentions
C++ 11: 1 mentions
Boost: 1 mentions
Xcode: 2 mentions
gcc: 1 mentions
OO&AD: 1 mentions
.NET: 20 mentions
Unity 5: 2 mentions
Mono: 3 mentions
MonoDevelop: 1 mentions
Xamarin: 1 mentions
TOTAL: 180 mentions
```
### Validating
HackMyResume can also validate your resumes against either the [FRESH /
FRESCA][fresca] or [JSON Resume][6] formats. To validate one or more existing
resumes, use the `validate` command:
```bash
# Validate myresume.json against either the FRESH or JSON Resume schema.
hackmyresume validate resumeA.json resumeB.json
```
HackMyResume will validate each specified resume in turn:
```bash
*** HackMyResume v1.6.0 ***
Validating JSON resume: resumeA.json (INVALID)
Validating JSON resume: resumeB.json (VALID)
```
### Converting
HackMyResume can convert between the [FRESH][fresca] and [JSON Resume][6]
formats. Just run:
```bash
hackmyresume convert <INPUTS> <OUTPUTS>
```
where <INPUTS> is one or more resumes in FRESH or JSON Resume format, and
<OUTPUTS> is a corresponding list of output file names. HackMyResume will
autodetect the format (FRESH or JRS) of each input resume and convert it to the
other format (JRS or FRESH).
### File-based Options
You can pass options into HackMyResume via an external options or ".hackmyrc"
file with the `--options` or `-o` switch:
```bash
hackmyresume build resume.json -o path/to/options.json
```
The options file can contain any documented HackMyResume option, including
`theme`, `silent`, `debug`, `pdf`, `css`, and other settings.
```json
{
"theme": "compact",
"sectionTitles": {
"employment": "Work"
},
"wkhtmltopdf": {
"margin-top": "20mm"
}
}
```
If an option is specified on both the command line and in an external options
file, the command-line option wins.
```bash
# path/to/options.json specifes the POSITIVE theme
# -t parameter specifies the COMPACT theme
# The -t parameter wins.
hackmyresume build resume.json -o path/to/options.json -t compact
> Reading resume: resume.json
> Applying COMPACT theme (7 formats)
```
### Prettifying
FluentCMD applies [js-beautify][10]-style HTML prettification by default to HTML-formatted resumes. To disable prettification, the `--nopretty` or `-n` flag can be used:
HackMyResume applies [js-beautify][10]-style HTML prettification by default to
HTML-formatted resumes. To disable prettification, the `--no-prettify` or `-n`
flag can be used:
```bash
fluentcmd resume.json out.all --nopretty
hackmyresume build resume.json out.all --no-prettify
```
### Silent Mode
@ -158,10 +528,91 @@ fluentcmd resume.json out.all --nopretty
Use `-s` or `--silent` to run in silent mode:
```bash
fluentcmd resume.json -o someFile.all -s
fluentcmd resume.json -o someFile.all --silent
hackmyresume build resume.json -o someFile.all -s
hackmyresume build resume.json -o someFile.all --silent
```
### Debug Mode
Use `-d` or `--debug` to force HMR to emit a call stack when errors occur. In
the future, this option will emit detailed error logging.
```bash
hackmyresume build resume.json -d
hackmyresume analyze resume.json --debug
```
### Disable Encoding
Use the `--no-escape` option to disable encoding in Handlebars themes. Note:
this option has no effect for non-Handlebars themes.
```bash
hackmyresume build resume.json --no-escape
```
### Private Resume Fields
Have a gig, education stint, membership, or other relevant history that you'd
like to hide from *most* (e.g. public) resumes but sometimes show on others? Tag it with
`"private": true` to omit it from outbound generated resumes by default.
```json
"employment": {
"history": [
{
"employer": "Acme Real Estate"
},
{
"employer": "Area 51 Alien Research Laboratory",
"private": true
},
{
"employer": "H&R Block"
}
]
}
```
Then, when you want a copy of your resume that includes the private gig / stint
/ etc., tell HackMyResume that it's OK to emit private fields. The way you do
that is with the `--private` switch.
```bash
hackmyresume build resume.json private-resume.all --private
```
### Custom theme helpers
You can attach your own custom Handlebars helpers to a FRESH theme with the
`helpers` key of your theme's `theme.json` file.
```js
{
"title": "my-cool-theme",
// ...
"helpers": [
"../path/to/helpers/*.js",
"some-other-helper.js"
]
}
```
HackMyResume will attempt to load each path or glob and register any specified
files with [Handlebars.registerHelper][hrh], making them available to your
theme.
## Contributing
HackMyResume is a community-driven free and open source project under the MIT
License. Contributions are encouraged and we respond to all PRs and issues in
time. See [CONTRIBUTING.md][contribute] for details.
## License
MIT. Go crazy. See [LICENSE.md][1] for details.
@ -176,3 +627,19 @@ MIT. Go crazy. See [LICENSE.md][1] for details.
[8]: https://youtu.be/N9wsjroVlu8
[9]: https://api.jquery.com/jquery.extend/
[10]: https://github.com/beautify-web/js-beautify
[11]: http://weasyprint.org/
[fresh]: https://github.com/fluentdesk/FRESH
[fresca]: https://github.com/fresh-standard/fresh-resume-schema
[dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
[img-release]: https://img.shields.io/github/release/hacksalot/HackMyResume.svg?label=version
[img-master]: https://img.shields.io/travis/hacksalot/HackMyResume/master.svg
[img-dev]: https://img.shields.io/travis/hacksalot/HackMyResume/dev.svg?label=dev
[travis-url-master]: https://travis-ci.org/hacksalot/HackMyResume?branch=master
[travis-url-dev]: https://travis-ci.org/hacksalot/HackMyResume?branch=dev
[latest-release]: https://github.com/hacksalot/HackMyResume/releases/latest
[contribute]: CONTRIBUTING.md
[fresh-themes]: https://github.com/fluentdesk/fresh-themes
[jrst]: https://www.npmjs.com/search?q=jsonresume-theme
[gh]: https://gitter.im/hacksalot/HackMyResume?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
[badge]: https://badges.gitter.im/hacksalot/HackMyResume.svg
[hrh]: http://handlebarsjs.com/reference.html#base-registerHelper

107
ROADMAP.md Normal file
View File

@ -0,0 +1,107 @@
Development Roadmap
===================
## Short-Term
### FluentCV Desktop: Beta 1
The **FluentCV Desktop 1.0 beta release** will present HackMyResume
functionality in a cross-platform desktop application for OS X, Linux, and
Windows.
### GitHub Integration
HackMyResume will offer GitHub integration for versioned resume storage and
retrieval via the `COMMIT` or `STORE` command(s) starting in 1.7.0 or 1.8.0.
### fresh-themes 1.0.0
The **fresh-themes 1.0** release will bring 100% coverage of the FRESH and JRS
object models&mdash;all resume sections and fields&mdash;along with
documentation, theme developer's guide, new themes, and a freeze to the FRESH
theme structure.
### Better LaTeX support
Including Markdown-to-LaTeX translation and more LaTeX-driven themes / formats.
### StackOverflow and LinkedIn support
Will start appearing in v1.7.0, with incremental improvements in 1.8.0 and
beyond.
### Improved resume sorting and arranging
**Better resume sorting** of items and sections: ascending, descending, by
date or other criteria ([#67][i67]).
### Remote resume / theme loading
Support remote loading of themes and resumes over `http`, `https`, and
`git://`. Enable these usage patterns:
```bash
hackmyresume build https://somesite.com/my-resume.json -t informatic
hackmyresume build resume.json -t npm:fresh-theme-ergonomic
hackmyresume analyze https://github.com/foo/my-resume
```
### 100% code coverage
Should reduce certain classes of errors and allow HMR to display a nifty 100%
code coverage badge.
### Improved **documentation and samples**
Expanded documentation and samples throughout.
## Mid-Term
### Cover letters and job descriptions
Add support for schema-driven **cover letters** and **job descriptions**.
### Character Sheets
HackMyResume 2.0 will ship with support for, yes, RPG-style character sheets.
This will demonstrate the tool's ability to flow arbitrary JSON to concrete
document(s) and provide unique albeit niche functionality around various games
([#117][i117]).
### Rich text (.rtf) output formats
Basic support for **rich text** `.rtf` output formats.
### Investigate: groff support
Investigate adding [**groff**][groff] support, because that would, indeed, be
[dope][d] ([#37][i37]).
### Investigate: org-mode support
Investigate adding [**org mode**][om] support ([#38][i38]).
### Investigate: Scribus
Investigate adding [**Scribus SLA**][scri] support ([#54][i54]).
### Support JSON Resume 1.0.0
When released.
## Long-Term
- TBD
[groff]: http://www.gnu.org/software/groff/
[om]: http://orgmode.org/
[scri]: https://en.wikipedia.org/wiki/Scribus
[d]: https://github.com/hacksalot/HackMyResume/issues/37#issue-123818674
[i37]: https://github.com/hacksalot/HackMyResume/issues/37
[i38]: https://github.com/hacksalot/HackMyResume/issues/38
[i54]: https://github.com/hacksalot/HackMyResume/issues/54
[i67]: https://github.com/hacksalot/HackMyResume/issues/67
[i107]: https://github.com/hacksalot/HackMyResume/issues/107
[i117]: https://github.com/hacksalot/HackMyResume/issues/117

BIN
assets/hmr_analyze.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/hmr_build.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a476ee59e7d86b5a7599780b5efca57ee6b6d60e1a722343277057ea793703b6
size 1642116

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
assets/resume-bouqet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

2800
npm-shrinkwrap.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,109 @@
{
"name": "fluentcmd",
"version": "0.5.0",
"description": "Generate beautiful, targeted resumes from your command line or shell.",
"name": "hackmyresume",
"version": "1.9.0-beta",
"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.",
"repository": {
"type": "git",
"url": "https://github.com/fluentdesk/fluentcmd.git"
"url": "https://github.com/hacksalot/HackMyResume.git"
},
"scripts": {
"test": "grunt clean:test && mocha --exit",
"grunt": "grunt"
},
"keywords": [
"resume",
"CV",
"portfolio",
"Markdown"
"employment",
"career",
"Markdown",
"JSON",
"Word",
"PDF",
"YAML",
"HTML",
"LaTeX",
"CLI",
"Handlebars",
"Underscore",
"template"
],
"author": "hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)",
"contributors": [
"aruberto (https://github.com/aruberto)",
"daniele-rapagnani (https://github.com/daniele-rapagnani)",
"jjanusch (https://github.com/driftdev)",
"robertmain (https://github.com/robertmain)",
"tomheon (https://github.com/tomheon)",
"zhuangya (https://github.com/zhuangya)",
"hacksalot <hacksalot@indevious.com> (https://github.com/hacksalot)"
],
"author": "James M. Devlin",
"license": "MIT",
"preferGlobal": "true",
"bugs": {
"url": "https://github.com/fluentdesk/fluentcmd/issues"
"url": "https://github.com/hacksalot/HackMyResume/issues"
},
"main": "src/fluentcmd.js",
"bin": {
"fluentcmd": "src/index.js"
"hackmyresume": "src/cli/index.js"
},
"homepage": "https://github.com/fluentdesk/fluentcmd",
"main": "src/index.js",
"homepage": "https://github.com/hacksalot/HackMyResume",
"dependencies": {
"fluentlib": "fluentdesk/fluentlib#v0.4.0",
"minimist": "^1.2.0",
"chalk": "^2.3.1",
"commander": "^2.9.0",
"copy": "^0.3.1",
"escape-latex": "^1.0.0",
"extend": "^3.0.0",
"fresh-jrs-converter": "^1.0.0",
"fresh-resume-schema": "^1.0.0-beta",
"fresh-resume-starter": "^0.3.1",
"fresh-resume-validator": "^0.2.0",
"fresh-themes": "^0.17.0-beta",
"fs-extra": "^5.0.0",
"glob": "^7.1.2",
"handlebars": "^4.0.5",
"html": "^1.0.0",
"is-my-json-valid": "^2.12.4",
"json-lint": "^0.1.0",
"jsonlint": "^1.6.2",
"lodash": "^4.17.5",
"marked": "^0.3.5",
"mkdirp": "^0.5.1",
"moment": "^2.11.1",
"parse-filepath": "^1.0.2",
"path-exists": "^3.0.0",
"pinkie-promise": "^2.0.0",
"printf": "^0.2.3",
"recursive-readdir-sync": "^1.0.6",
"simple-html-tokenizer": "^0.4.3",
"slash": "^1.0.0",
"string-padding": "^1.0.2",
"string.prototype.endswith": "^0.2.0",
"string.prototype.startswith": "^0.2.0",
"traverse": "^0.6.6",
"underscore": "^1.8.3",
"watermark": "fluentdesk/watermark#v0.3.1-alpha"
"word-wrap": "^1.1.0",
"xml-escape": "^1.0.0",
"yamljs": "^0.3.0"
},
"devDependencies": {
"chai": "*",
"chai-as-promised": "^7.1.1",
"dir-compare": "^1.4.0",
"fresh-test-resumes": "^0.9.2",
"fresh-test-themes": "^0.2.0",
"fresh-theme-underscore": "^0.1.1",
"grunt": "*",
"grunt-contrib-clean": "^1.1.0",
"grunt-contrib-coffee": "^2.0.0",
"grunt-contrib-copy": "^1.0.0",
"grunt-eslint": "^20.1.0",
"grunt-simple-mocha": "*",
"jsonresume-theme-boilerplate": "^0.1.2",
"jsonresume-theme-classy": "^1.0.9",
"jsonresume-theme-modern": "0.0.18",
"jsonresume-theme-sceptile": "^1.0.5",
"mocha": "*",
"stripcolorcodes": "^0.1.0"
}
}

30
src/cli/analyze.hbs Normal file
View File

@ -0,0 +1,30 @@
{{style "SECTIONS (" "bold"}}{{style totals.numSections "white" }}{{style ")" "bold"}}
employment: {{v totals.totals.employment "-" 2 "bold" }}
projects: {{v totals.totals.projects "-" 2 "bold" }}
education: {{v totals.totals.education "-" 2 "bold" }}
service: {{v totals.totals.service "-" 2 "bold" }}
skills: {{v totals.totals.skills "-" 2 "bold" }}
writing: {{v totals.totals.writing "-" 2 "bold" }}
speaking: {{v totals.totals.speaking "-" 2 "bold" }}
reading: {{v totals.totals.reading "-" 2 "bold" }}
social: {{v totals.totals.social "-" 2 "bold" }}
references: {{v totals.totals.references "-" 2 "bold" }}
testimonials: {{v totals.totals.testimonials "-" 2 "bold" }}
languages: {{v totals.totals.languages "-" 2 "bold" }}
interests: {{v totals.totals.interests "-" 2 "bold" }}
{{style "COVERAGE (" "bold"}}{{style coverage.pct "white"}}{{style ")" "bold"}}
Total Days: {{v coverage.duration.total "-" 5 "bold" }}
Employed: {{v coverage.duration.work "-" 5 "bold" }}
Gaps: {{v coverage.gaps.length "-" 5 "bold" }} [{{#if coverage.gaps.length }}{{#each coverage.gaps }}{{#unless @first}} {{/unless}}{{gapLength duration }}{{/each}}{{/if}}]
Overlaps: {{v coverage.overlaps.length "-" 5 "bold" }} [{{#if coverage.overlaps.length }}{{#each coverage.overlaps }}{{#unless @first}} {{/unless}}{{gapLength duration }}{{/each}}{{/if}}]
{{style "KEYWORDS (" "bold"}}{{style keywords.length "white" }}{{style ")" "bold"}}
{{#each keywords }}{{{pad name 18}}}: {{v count "-" 5 "bold"}} mention{{#isPlural count}}s{{/isPlural}}
{{/each}}
-------------------------------
{{v keywords.length "0" 9 "bold"}} {{style "KEYWORDS" "bold"}} {{v keywords.totalKeywords "0" 5 "bold"}} {{style "mentions" "bold"}}

328
src/cli/error.js Normal file
View File

@ -0,0 +1,328 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Error-handling routines for HackMyResume.
@module cli/error
@license MIT. See LICENSE.md for details.
*/
const HMSTATUS = require('../core/status-codes');
const FS = require('fs');
const PATH = require('path');
const WRAP = require('word-wrap');
const M2C = require('../utils/md2chalk');
const chalk = require('chalk');
const extend = require('extend');
const printf = require('printf');
const SyntaxErrorEx = require('../utils/syntax-error-ex');
require('string.prototype.startswith');
/** Error handler for HackMyResume. All errors are handled here.
@class ErrorHandler */
module.exports = {
init( debug, assert, silent ) {
this.debug = debug;
this.assert = assert;
this.silent = silent;
this.msgs = require('./msg').errors;
return this;
},
err( ex, shouldExit ) {
// Short-circuit logging output if --silent is on
let stack;
const o = this.silent ? function() {} : _defaultLog;
// Special case; can probably be removed.
if (ex.pass) { throw ex; }
// Load error messages
this.msgs = this.msgs || require('./msg').errors;
// Handle packaged HMR exceptions
if (ex.fluenterror) {
// Output the error message
const objError = assembleError.call(this, ex);
o( this[ `format_${objError.etype}` ]( objError.msg ));
// Output the stack (sometimes)
if (objError.withStack) {
stack = ex.stack || (ex.inner && ex.inner.stack);
stack && o( chalk.gray( stack ) );
}
// Quit if necessary
if (shouldExit || ex.exit) {
if (this.debug) {
o(chalk.cyan(`Exiting with error code ${ex.fluenterror.toString()}`));
}
if (this.assert) {
ex.pass = true;
throw ex;
}
return process.exit(ex.fluenterror);
}
// Handle raw exceptions
} else {
o(ex);
const stackTrace = ex.stack || (ex.inner && ex.inner.stack);
if (stackTrace && this.debug) {
return o(M2C(ex.stack || ex.inner.stack, 'gray'));
}
}
},
format_error( msg ) {
msg = msg || '';
return chalk.red.bold( msg.toUpperCase().startsWith('ERROR:') ? msg : `Error: ${msg}` );
},
format_warning( brief, msg ) {
return chalk.yellow(brief) + chalk.yellow(msg || '');
},
format_custom( msg ) { return msg; }
};
var _defaultLog = function() { return console.log.apply(console.log, arguments); }; // eslint-disable-line no-console
var assembleError = function( ex ) {
let se;
let msg = '';
let withStack = false;
let quit = false;
let etype = 'warning';
if (this.debug) { withStack = true; }
switch (ex.fluenterror) {
case HMSTATUS.themeNotFound:
msg = printf( M2C( this.msgs.themeNotFound.msg, 'yellow' ), ex.data);
break;
case HMSTATUS.copyCSS:
msg = M2C( this.msgs.copyCSS.msg, 'red' );
quit = false;
break;
case HMSTATUS.resumeNotFound:
//msg = M2C( this.msgs.resumeNotFound.msg, 'yellow' );
msg += M2C(FS.readFileSync(
PATH.resolve(__dirname, `help/${ex.verb}.txt`), 'utf8' ), 'white', 'yellow');
break;
case HMSTATUS.missingCommand:
// msg = M2C( this.msgs.missingCommand.msg + " (", 'yellow');
// msg += Object.keys( FCMD.verbs ).map( (v, idx, ar) ->
// return ( if idx == ar.length - 1 then chalk.yellow('or ') else '') +
// chalk.yellow.bold(v.toUpperCase());
// ).join( chalk.yellow(', ')) + chalk.yellow(").\n\n");
msg += M2C(FS.readFileSync(
PATH.resolve(__dirname, 'help/use.txt'), 'utf8' ), 'white', 'yellow');
break;
case HMSTATUS.invalidCommand:
msg = printf( M2C( this.msgs.invalidCommand.msg, 'yellow'), ex.attempted );
break;
case HMSTATUS.resumeNotFoundAlt:
msg = M2C( this.msgs.resumeNotFoundAlt.msg, 'yellow' );
break;
case HMSTATUS.inputOutputParity:
msg = M2C( this.msgs.inputOutputParity.msg );
break;
case HMSTATUS.createNameMissing:
msg = M2C( this.msgs.createNameMissing.msg );
break;
case HMSTATUS.pdfGeneration:
msg = M2C( this.msgs.pdfGeneration.msg, 'bold' );
if (ex.inner) { msg += chalk.red(`\n${ex.inner}`); }
quit = false;
etype = 'error';
break;
case HMSTATUS.invalid:
msg = M2C( this.msgs.invalid.msg, 'red' );
etype = 'error';
break;
case HMSTATUS.generateError:
msg = (ex.inner && ex.inner.toString()) || ex;
quit = false;
etype = 'error';
break;
case HMSTATUS.fileSaveError:
msg = printf( M2C( this.msgs.fileSaveError.msg ), (ex.inner || ex).toString() );
etype = 'error';
quit = false;
break;
case HMSTATUS.invalidFormat:
ex.data.forEach( function(d) {
return msg += printf( M2C( this.msgs.invalidFormat.msg, 'bold' ),
ex.theme.name.toUpperCase(), d.format.toUpperCase());
}
, this);
break;
case HMSTATUS.missingParam:
msg = printf(M2C( this.msgs.missingParam.msg ), ex.expected, ex.helper);
break;
case HMSTATUS.invalidHelperUse:
msg = printf( M2C( this.msgs.invalidHelperUse.msg ), ex.helper );
if (ex.error) {
msg += `\n--> ${assembleError.call( this, extend( true, {}, ex, {fluenterror: ex.error} )).msg}`;
}
//msg += printf( '\n--> ' + M2C( this.msgs.invalidParamCount.msg ), ex.expected );
quit = false;
etype = 'warning';
break;
case HMSTATUS.notOnPath:
msg = printf( M2C(this.msgs.notOnPath.msg, 'bold'), ex.engine);
quit = false;
etype = 'error';
break;
case HMSTATUS.readError:
if (!ex.quiet) {
// eslint-disable-next-line no-console
console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file));
}
msg = ex.inner.toString();
etype = 'error';
break;
case HMSTATUS.mixedMerge:
msg = M2C(this.msgs.mixedMerge.msg);
quit = false;
break;
case HMSTATUS.invokeTemplate:
msg = M2C(this.msgs.invokeTemplate.msg, 'red');
msg += M2C( `\n${WRAP(ex.inner.toString(), { width: 60, indent: ' ' })}`, 'gray' );
etype = 'custom';
break;
case HMSTATUS.compileTemplate:
etype = 'error';
break;
case HMSTATUS.themeLoad:
msg = M2C( printf( this.msgs.themeLoad.msg, ex.attempted.toUpperCase() ), 'red');
if (ex.inner && ex.inner.fluenterror) {
msg += M2C('\nError: ', 'red') + assembleError.call( this, ex.inner ).msg;
}
quit = true;
etype = 'custom';
break;
case HMSTATUS.parseError:
if (SyntaxErrorEx.is(ex.inner)) {
// eslint-disable-next-line no-console
console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file ));
se = new SyntaxErrorEx(ex, ex.raw);
if ((se.line != null) && (se.col != null)) {
msg = printf(M2C( this.msgs.parseError.msg[0], 'red' ), se.line, se.col);
} else if (se.line != null) {
msg = printf(M2C( this.msgs.parseError.msg[1], 'red' ), se.line);
} else {
msg = M2C(this.msgs.parseError.msg[2], 'red');
}
} else if (ex.inner && (ex.inner.line != null) && (ex.inner.col != null)) {
msg = printf( M2C( this.msgs.parseError.msg[0], 'red' ), ex.inner.line, ex.inner.col);
} else {
msg = ex;
}
etype = 'error';
break;
case HMSTATUS.createError:
// inner.code could be EPERM, EACCES, etc
msg = printf(M2C( this.msgs.createError.msg ), ex.inner.path);
etype = 'error';
break;
case HMSTATUS.validateError:
msg = printf(M2C( this.msgs.validateError.msg ), ex.inner.toString());
etype = 'error';
break;
case HMSTATUS.invalidOptionsFile:
msg = M2C(this.msgs.invalidOptionsFile.msg[0]);
if (SyntaxErrorEx.is(ex.inner)) {
// eslint-disable-next-line no-console
console.error(printf( M2C(this.msgs.readError.msg, 'red'), ex.file ));
se = new SyntaxErrorEx(ex, ex.raw);
if ((se.line != null) && (se.col != null)) {
msg += printf(M2C( this.msgs.parseError.msg[0], 'red' ), se.line, se.col);
} else if (se.line != null) {
msg += printf(M2C( this.msgs.parseError.msg[1], 'red' ), se.line);
} else {
msg += M2C(this.msgs.parseError.msg[2], 'red');
}
} else if (ex.inner && (ex.inner.line != null) && (ex.inner.col != null)) {
msg += printf( M2C( this.msgs.parseError.msg[0], 'red' ), ex.inner.line, ex.inner.col);
} else {
msg += ex;
}
msg += this.msgs.invalidOptionsFile.msg[1];
etype = 'error';
break;
case HMSTATUS.optionsFileNotFound:
msg = M2C( this.msgs.optionsFileNotFound.msg );
etype = 'error';
break;
case HMSTATUS.unknownSchema:
msg = M2C( this.msgs.unknownSchema.msg[0] );
//msg += "\n" + M2C( @msgs.unknownSchema.msg[1], 'yellow' )
etype = 'error';
break;
case HMSTATUS.themeHelperLoad:
msg = printf(M2C( this.msgs.themeHelperLoad.msg ), ex.glob);
etype = 'error';
break;
case HMSTATUS.invalidSchemaVersion:
msg = printf(M2C( this.msgs.invalidSchemaVersion.msg ), ex.data);
etype = 'error';
break;
}
return {
msg, // The error message to display
withStack, // Whether to include the stack
quit,
etype
};
};

25
src/cli/help/analyze.txt Normal file
View File

@ -0,0 +1,25 @@
**analyze** | Analyze a resume for statistical insight
Usage:
**hackmyresume ANALYZE <resume>**
The ANALYZE command evaluates the specified resume(s) for
coverage, duration, gaps, keywords, and other metrics.
This command can be run against multiple resumes. Each
will be analyzed in turn.
Parameters:
**<resume>**
Path to a FRESH or JRS resume. Multiple resumes can be
specified, separated by spaces.
hackmyresume ANALYZE resume.json
hackmyresume ANALYZE r1.json r2.json r3.json
Options:
**None.**

69
src/cli/help/build.txt Normal file
View File

@ -0,0 +1,69 @@
**build** | Generate themed resumes in multiple formats
Usage:
**hackmyresume BUILD <resume> TO <target> [--theme]**
**[--pdf] [--no-escape] [--private]**
The BUILD command generates themed resumes and CVs in
multiple formats. Use it to create outbound resumes in
specific formats such HTML, MS Word, and PDF.
Parameters:
**<resume>**
Path to a FRESH or JRS resume (*.json) containing your
resume data. Multiple resumes may be specified.
If multiple resumes are specified, they will be merged
into a single resume prior to transformation.
**<target>**
Path to the desired output resume. Multiple resumes
may be specified. The file extension will determine
the format.
.all Generate all supported formats
.html HTML 5
.doc MS Word
.pdf Adobe Acrobat PDF
.txt plain text
.md Markdown
.png PNG Image
.latex LaTeX
Note: not all formats are supported by all themes!
Check the theme's documentation for details or use
the .all extension to build all available formats.
Options:
**--theme -t <theme-name-or-path>**
Path to a FRESH or JSON Resume theme OR the name of a
built-in theme. Valid theme names are 'modern',
'positive', 'compact', 'awesome', and 'basis'.
**--pdf -p <engine>**
Specify the PDF engine to use. Legal values are
'none', 'wkhtmltopdf', 'phantom', or 'weasyprint'.
**--no-escape**
Disable escaping / encoding of resume data during
resume generation. Handlebars themes only.
**--private**
Include resume fields marked as private.
Notes:
The BUILD command can be run against multiple source as well
as multiple target resumes. If multiple source resumes are
provided, they will be merged into a single source resume
before generation. If multiple output resumes are provided,
each will be generated in turn.

33
src/cli/help/convert.txt Normal file
View File

@ -0,0 +1,33 @@
**convert** | Convert resumes between FRESH and JRS formats
Usage:
**hackmyresume CONVERT <resume> TO <target> [--format]**
The CONVERT command converts one or more resume documents
between the FRESH Resume Schema and JSON Resume formats.
Parameters:
**<resume>**
Path to a FRESH or JRS resume. Multiple resumes can be
specified.
**<targets>**
The path of the converted resume. Multiple resumes can
be specified, one per provided input resume.
Options:
**--format -f <fmt>**
The desired format for the new resume(s). Valid values
are 'FRESH', 'JRS', or, to target the latest edge
version of the JSON Resume Schema, 'JRS@1'.
If this parameter is omitted, the destination format
will be inferred from the source resume's format. If
the source format is FRESH, the destination format
will be JSON Resume, and vice-versa.

23
src/cli/help/help.txt Normal file
View File

@ -0,0 +1,23 @@
**help** | View help on a specific HackMyResume command
Usage:
**hackmyresume HELP [<command>]**
The HELP command displays help information for a specific
HackMyResume command, including the HELP command itself.
Parameters:
**<command>**
The HackMyResume command to view help information for.
Must be BUILD, NEW, CONVERT, ANALYZE, VALIDATE, PEEK,
or HELP.
hackmyresume help convert
hackmyresume help help
Options:
**None.**

29
src/cli/help/new.txt Normal file
View File

@ -0,0 +1,29 @@
**new** | Create a new FRESH or JRS resume document
Usage:
**hackmyresume NEW <fileName> [--format]**
The NEW command generates a new resume document in FRESH
or JSON Resume format. This document can serve as an
official source of truth for your resume and career data
as well an input to tools like HackMyResume.
Parameters:
**<fileName>**
The filename (relative or absolute path) of the resume
to be created. Multiple resume paths can be specified,
and each will be created in turn.
hackmyresume NEW resume.json
hackmyresume NEW r1.json foo/r2.json ../r3.json
Options:
**--format -f <fmt>**
The desired format for the new resume(s). Valid values
are 'FRESH', 'JRS', or, to target the latest edge
version of the JSON Resume Schema, 'JRS@1'.

31
src/cli/help/peek.txt Normal file
View File

@ -0,0 +1,31 @@
**peek** | View portions of a resume from the command line
Usage:
**hackmyresume PEEK <resume> <at>**
The PEEK command displays a specific piece or part of the
resume without requiring the resume to be opened in an
editor.
Parameters:
**<resume>**
Path to a FRESH or JRS resume. Multiple resumes can be
specified, separated by spaces.
hackmyresume PEEK r1.json r2.json r3.json "employment.history[2]"
**<at>**
The resume property or field to be displayed. Can be
any valid resume path, for example:
education[0]
info.name
employment.history[3].start
Options:
**None.**

70
src/cli/help/use.txt Normal file
View File

@ -0,0 +1,70 @@
**HackMyResume** | A Swiss Army knife for resumes and CVs
Usage:
**hackmyresume [--version] [--help] [--silent] [--debug]**
**[--options] [--no-colors] <command> [<args>]**
Commands: (type "hackmyresume help COMMAND" for details)
**BUILD** Build your resume to the destination format(s).
**ANALYZE** Analyze your resume for keywords, gaps, and metrics.
**VALIDATE** Validate your resume for errors and typos.
**NEW** Create a new resume in FRESH or JSON Resume format.
**CONVERT** Convert your resume between FRESH and JSON Resume.
**PEEK** View a specific field or element on your resume.
**HELP** View help on a specific HackMyResume command.
Common Tasks:
Generate a resume in a specific format (HTML, Word, PDF, etc.)
**hackmyresume build rez.json to out/rez.html**
**hackmyresume build rez.json to out/rez.doc**
**hackmyresume build rez.json to out/rez.pdf**
**hackmyresume build rez.json to out/rez.txt**
**hackmyresume build rez.json to out/rez.md**
**hackmyresume build rez.json to out/rez.png**
**hackmyresume build rez.json to out/rez.tex**
Build a resume to ALL available formats:
**hackmyresume build rez.json to out/rez.all**
Build a resume with a specific theme:
**hackmyresume build rez.json to out/rez.all -t themeName**
Create a new empty resume:
**hackmyresume new rez.json**
Convert a resume between FRESH and JRS formats:
**hackmyresume convert rez.json converted.json**
Analyze a resume for important metrics
**hackmyresume analyze rez.json**
Find more resume themes:
**https://www.npmjs.com/search?q=jsonresume-theme**
**https://www.npmjs.com/search?q=fresh-theme**
**https://github.com/fresh-standard/fresh-themes**
Validate a resume's structure and syntax:
**hackmyresume validate resume.json**
View help on a specific command:
**hackmyresume help [build|convert|new|analyze|validate|peek|help]**
Submit a bug or request:
**https://githut.com/hacksalot/HackMyResume/issues**
HackMyResume is free and open source software published
under the MIT license. For more information, visit the
HackMyResume website or GitHub project page.

26
src/cli/help/validate.txt Normal file
View File

@ -0,0 +1,26 @@
**validate** | Validate a resume for correctness
Usage:
**hackmyresume VALIDATE <resume> [--assert]**
The VALIDATE command validates a FRESH or JRS document
against its governing schema, verifying that the resume
is correctly structured and formatted.
Parameters:
**<resume>**
Path to a FRESH or JRS resume. Multiple resumes can be
specified.
hackmyresume ANALYZE resume.json
hackmyresume ANALYZE r1.json r2.json r3.json
Options:
**--assert -a**
Tell HackMyResume to return a non-zero process exit
code if a resume fails to validate.

22
src/cli/index.js Normal file
View File

@ -0,0 +1,22 @@
#! /usr/bin/env node
/**
Command-line interface (CLI) for HackMyResume.
@license MIT. See LICENSE.md for details.
@module index.js
*/
try {
require('./main')( process.argv );
}
catch( ex ) {
require('./error').err( ex, true );
}

421
src/cli/main.js Normal file
View File

@ -0,0 +1,421 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the `main` function.
@module cli/main
@license MIT. See LICENSE.md for details.
*/
const HMR = require('../index');
const PKG = require('../../package.json');
const FS = require('fs');
const EXTEND = require('extend');
const chalk = require('chalk');
const PATH = require('path');
const HMSTATUS = require('../core/status-codes');
const safeLoadJSON = require('../utils/safe-json-loader');
//StringUtils = require '../utils/string.js'
const _ = require('underscore');
const OUTPUT = require('./out');
const PAD = require('string-padding');
const { Command } = require('commander');
const M2C = require('../utils/md2chalk');
const printf = require('printf');
const _opts = { };
const _title = chalk.white.bold(`\n*** HackMyResume v${PKG.version} ***`);
const _out = new OUTPUT( _opts );
const _err = require('./error');
let _exitCallback = null;
/*
A callable implementation of the HackMyResume CLI. Encapsulates the command
line interface as a single method accepting a parameter array.
@alias module:cli/main.main
@param rawArgs {Array} An array of command-line parameters. Will either be
process.argv (in production) or custom parameters (in test).
*/
module.exports = function( rawArgs, exitCallback ) {
const initInfo = initialize( rawArgs, exitCallback );
if (initInfo === null) {
return;
}
const { args } = initInfo;
// Create the top-level (application) command...
const program = new Command('hackmyresume')
.version(PKG.version)
.description(chalk.yellow.bold('*** HackMyResume ***'))
.option('-s --silent', 'Run in silent mode')
.option('--no-color', 'Disable colors')
.option('--color', 'Enable colors')
.option('-d --debug', 'Enable diagnostics', false)
.option('-a --assert', 'Treat warnings as errors', false)
.option('-v --version', 'Show the version')
.allowUnknownOption();
program.jsonArgs = initInfo.options;
// Create the NEW command
program
.command('new')
.arguments('<sources...>')
.option('-f --format <fmt>', 'FRESH or JRS format', 'FRESH')
.alias('create')
.description('Create resume(s) in FRESH or JSON RESUME format.')
.action((function( sources ) {
execute.call( this, sources, [], this.opts(), logMsg);
})
);
// Create the VALIDATE command
program
.command('validate')
.arguments('<sources...>')
.description('Validate a resume in FRESH or JSON RESUME format.')
.action(function(sources) {
execute.call( this, sources, [], this.opts(), logMsg);
});
// Create the CONVERT command
program
.command('convert')
.description('Convert a resume to/from FRESH or JSON RESUME format.')
.option('-f --format <fmt>', 'FRESH or JRS format and optional version', undefined)
.action(function() {
const x = splitSrcDest.call( this );
execute.call( this, x.src, x.dst, this.opts(), logMsg);
});
// Create the ANALYZE command
program
.command('analyze')
.arguments('<sources...>')
.option('--private', 'Include resume fields marked as private', false)
.description('Analyze one or more resumes.')
.action(function( sources ) {
execute.call( this, sources, [], this.opts(), logMsg);
});
// Create the PEEK command
program
.command('peek')
.arguments('<sources...>')
.description('Peek at a resume field or section')
//.action(( sources, sectionOrField ) ->
.action(function( sources ) {
const dst = (sources && (sources.length > 1)) ? [sources.pop()] : [];
execute.call( this, sources, dst, this.opts(), logMsg);
});
// Create the BUILD command
program
.command('build')
.alias('generate')
.option('-t --theme <theme>', 'Theme name or path')
.option('-n --no-prettify', 'Disable HTML prettification', true)
.option('-c --css <option>', 'CSS linking / embedding')
.option('-p --pdf <engine>', 'PDF generation engine')
.option('--no-sort', 'Sort resume sections by date', false)
.option('--tips', 'Display theme tips and warnings.', false)
.option('--private', 'Include resume fields marked as private', false)
.option('--no-escape', 'Turn off encoding in Handlebars themes.', false)
.description('Generate resume to multiple formats')
//.action(( sources, targets, options ) ->
.action(function() {
const x = splitSrcDest.call( this );
execute.call( this, x.src, x.dst, this.opts(), logMsg);
});
// Create the HELP command
program
.command('help')
.arguments('[command]')
.description('Get help on a HackMyResume command')
.action(function( cmd ) {
cmd = cmd || 'use';
const manPage = FS.readFileSync(
PATH.join(__dirname, `help/${cmd}.txt`),
'utf8');
_out.log(M2C(manPage, 'white', 'yellow.bold'));
});
program.parse( args );
if (!program.args.length) {
throw {fluenterror: 4};
}
};
/* Massage command-line args and setup Commander.js. */
var initialize = function( ar, exitCallback ) {
_exitCallback = exitCallback || process.exit;
const o = initOptions(ar);
if (o.ex) {
_err.init(false, true, false);
if( o.ex.op === 'parse' ) {
_err.err({
fluenterror: o.ex.op === 'parse' ? HMSTATUS.invalidOptionsFile : HMSTATUS.optionsFileNotFound,
inner: o.ex.inner,
quit: true
});
} else {
_err.err({fluenterror: HMSTATUS.optionsFileNotFound, inner: o.ex.inner, quit: true});
}
return null;
}
o.silent || logMsg( _title );
// Emit debug prelude if --debug was specified
if (o.debug) {
_out.log(chalk.cyan('The -d or --debug switch was specified. DEBUG mode engaged.'));
_out.log('');
_out.log(chalk.cyan(PAD(' Platform:',25, null, PAD.RIGHT)) + chalk.cyan.bold( process.platform === 'win32' ? 'windows' : process.platform ));
_out.log(chalk.cyan(PAD(' Node.js:',25, null, PAD.RIGHT)) + chalk.cyan.bold( process.version ));
_out.log(chalk.cyan(PAD(' HackMyResume:',25, null, PAD.RIGHT)) + chalk.cyan.bold(`v${PKG.version}` ));
_out.log(chalk.cyan(PAD(' FRESCA:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies.fresca ));
//_out.log(chalk.cyan(PAD(' fresh-themes:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-themes'] ))
//_out.log(chalk.cyan(PAD(' fresh-jrs-converter:',25, null, PAD.RIGHT)) + chalk.cyan.bold( PKG.dependencies['fresh-jrs-converter'] ))
_out.log('');
}
_err.init(o.debug, o.assert, o.silent);
// Handle invalid verbs here (a bit easier here than in commander.js)...
if (o.verb && !HMR.verbs[ o.verb ] && !HMR.alias[ o.verb ] && (o.verb !== 'help')) {
_err.err({fluenterror: HMSTATUS.invalidCommand, quit: true, attempted: o.orgVerb}, true);
}
// Override the .missingArgument behavior
Command.prototype.missingArgument = function() {
if (this.name() !== 'help') {
_err.err({
verb: this.name(),
fluenterror: HMSTATUS.resumeNotFound
}
, true);
}
};
// Override the .helpInformation behavior
Command.prototype.helpInformation = function() {
const manPage = FS.readFileSync(
PATH.join(__dirname, 'help/use.txt'), 'utf8' );
return M2C(manPage, 'white', 'yellow');
};
return {
args: o.args,
options: o.json
};
};
/* Init options prior to setting up command infrastructure. */
var initOptions = function( ar ) {
let oJSON, oVerb;
oVerb;
let verb = '';
const args = ar.slice();
const cleanArgs = args.slice( 2 );
oJSON;
if (cleanArgs.length) {
// Support case-insensitive sub-commands (build, generate, validate, etc)
const vidx = _.findIndex(cleanArgs, v => v[0] !== '-');
if (vidx !== -1) {
oVerb = cleanArgs[ vidx ];
verb = (args[ vidx + 2 ] = oVerb.trim().toLowerCase());
}
// Remove --options --opts -o and process separately
const optsIdx = _.findIndex(cleanArgs, v => (v === '-o') || (v === '--options') || (v === '--opts'));
if (optsIdx !== -1) {
let optStr = cleanArgs[ optsIdx + 1];
args.splice( optsIdx + 2, 2 );
if (optStr && (optStr = optStr.trim())) {
//var myJSON = JSON.parse(optStr);
if( optStr[0] === '{') {
// TODO: remove use of evil(). - hacksalot
/* jshint ignore:start */
oJSON = eval(`(${optStr})`); // jshint ignore:line <-- no worky
/* jshint ignore:end */
} else {
const inf = safeLoadJSON( optStr );
if( !inf.ex ) {
oJSON = inf.json;
} else {
return inf;
}
}
}
}
}
// Grab the --debug flag, --silent, --assert and --no-color flags
const isDebug = _.some(args, v => (v === '-d') || (v === '--debug'));
const isSilent = _.some(args, v => (v === '-s') || (v === '--silent'));
const isAssert = _.some(args, v => (v === '-a') || (v === '--assert'));
const isMono = _.some(args, v => v === '--no-color');
const isNoEscape = _.some(args, v => v === '--no-escape');
return {
color: !isMono,
debug: isDebug,
silent: isSilent,
assert: isAssert,
noescape: isNoEscape,
orgVerb: oVerb,
verb,
json: oJSON,
args
};
};
/* Invoke a HackMyResume verb. */
var execute = function( src, dst, opts, log ) {
// Create the verb
const v = new (HMR.verbs[ this.name() ])();
// Initialize command-specific options
loadOptions.call(this, opts, this.parent.jsonArgs);
// Set up error/output handling
_opts.errHandler = v;
_out.init(_opts);
// Hook up event notifications
v.on('hmr:status', function() { return _out.do.apply(_out, arguments); });
v.on('hmr:error', function() { return _err.err.apply(_err, arguments); });
// Invoke the verb using promise syntax
const prom = v.invoke.call(v, src, dst, _opts, log);
prom.then(executeSuccess, executeFail);
};
/* Success handler for verb invocations. Calls process.exit by default */
var executeSuccess = function() {};
// Can't call _exitCallback here (process.exit) when PDF is running in BK
//_exitCallback 0; return
/* Failure handler for verb invocations. Calls process.exit by default */
var executeFail = function(err) {
//console.dir err
let finalErrorCode = -1;
if (err) {
if (err.fluenterror) {
finalErrorCode = err.fluenterror;
} else if (err.length) {
finalErrorCode = err[0].fluenterror;
} else {
finalErrorCode = err;
}
}
if (_opts.debug) {
const msgs = require('./msg').errors;
logMsg(printf(M2C( msgs.exiting.msg, 'cyan' ), finalErrorCode));
if (err.stack) { logMsg(err.stack); }
}
_exitCallback(finalErrorCode);
};
/*
Initialize HackMyResume options.
TODO: Options loading is a little hacky, for two reasons:
- Commander.js idiosyncracies
- Need to accept JSON inputs from the command line.
*/
var loadOptions = function( o, cmdO ) {
// o and this.opts() seem to be the same (command-specific options)
// Load the specified options file (if any) and apply options
if( cmdO ) {
o = EXTEND(true, o, cmdO);
}
// Merge in command-line options
o = EXTEND( true, o, this.opts() );
// Kludge parent-level options until piping issue is resolved
if ((this.parent.silent !== undefined) && (this.parent.silent !== null)) {
o.silent = this.parent.silent;
}
if ((this.parent.debug !== undefined) && (this.parent.debug !== null)) {
o.debug = this.parent.debug;
}
if ((this.parent.assert !== undefined) && (this.parent.assert !== null)) {
o.assert = this.parent.assert;
}
if (o.debug) {
logMsg(chalk.cyan('OPTIONS:') + '\n');
_.each(o, (val, key) =>
logMsg(chalk.cyan(' %s') + chalk.cyan.bold(' %s'),
PAD(key,22,null,PAD.RIGHT), val)
);
logMsg('');
}
// Cache
EXTEND( true, _opts, o );
};
/* Split multiple command-line filenames by the 'TO' keyword */
var splitSrcDest = function() {
const params = this.parent.args.filter(j => String.is(j));
if (params.length === 0) {
//tmpName = @name()
throw { fluenterror: HMSTATUS.resumeNotFound, verb: this.name(), quit: true };
}
// Find the TO keyword, if any
const splitAt = _.findIndex( params, p => p.toLowerCase() === 'to');
// TO can't be the last keyword
if ((splitAt === (params.length - 1)) && (splitAt !== -1)) {
logMsg(chalk.yellow('Please ') +
chalk.yellow.bold('specify an output file') +
chalk.yellow(' for this operation or ') +
chalk.yellow.bold('omit the TO keyword') +
chalk.yellow('.') );
return;
}
return {
src: params.slice(0, splitAt === -1 ? undefined : splitAt ),
dst: splitAt === -1 ? [] : params.slice( splitAt + 1 )
};
};
/* Simple logging placeholder. */
var logMsg = function() {
// eslint-disable-next-line no-console
return _opts.silent || console.log.apply( console.log, arguments );
};

10
src/cli/msg.js Normal file
View File

@ -0,0 +1,10 @@
/**
Message-handling routines for HackMyResume.
@module cli/msg
@license MIT. See LICENSE.md for details.
*/
const PATH = require('path');
const YAML = require('yamljs');
module.exports = YAML.load(PATH.join(__dirname, 'msg.yml'));

141
src/cli/msg.yml Normal file
View File

@ -0,0 +1,141 @@
events:
begin:
msg: Invoking **%s** command.
beforeCreate:
msg: Creating new **%s** resume: **%s**
afterCreate:
msg: Creating new **%s** resume: **%s**
afterRead:
msg: Reading **%s** resume: **%s**
beforeTheme:
msg: Verifying **%s** theme.
afterTheme:
msg: Verifying outputs: ???
beforeMerge:
msg:
- "Merging **%s**"
- " onto **%s**"
applyTheme:
msg: Applying **%s** theme (**%s** format%s)
afterBuild:
msg:
- "The **%s** theme says:"
- |
"For best results view JSON Resume themes over a
local or remote HTTP connection. For example:
npm install http-server -g
http-server <resume-folder>
For more information, see the README."
afterGenerate:
msg:
- " (with %s)"
- "Skipping %s resume: %s"
- "Generating **%s** resume: **%s**"
beforeAnalyze:
msg: "Analyzing **%s** resume: **%s**"
beforeConvert:
msg: "Converting **%s** (**%s**) to **%s** (**%s**)"
afterValidate:
msg:
- "Validating **%s** against the **%s** schema: "
- "VALID!"
- "INVALID"
- "BROKEN"
- "MISSING"
- "ERROR"
beforePeek:
msg:
- Peeking at **%s** in **%s**
- Peeking at **%s**
afterPeek:
msg: "The specified key **%s** was not found in **%s**."
afterInlineConvert:
msg: Converting **%s** to **%s** format.
errors:
themeNotFound:
msg: >
**Couldn't find the '%s' theme.** Please specify the name of a preinstalled
FRESH theme or the path to a locally installed FRESH or JSON Resume theme.
copyCSS:
msg: Couldn't copy CSS file to destination folder.
resumeNotFound:
msg: Please **feed me a resume** in FRESH or JSON Resume format.
missingCommand:
msg: Please **give me a command**
invalidCommand:
msg: Invalid command: '%s'
resumeNotFoundAlt:
msg: Please **feed me a resume** in either FRESH or JSON Resume format.
inputOutputParity:
msg: Please **specify an output file name** for every input file you wish to convert.
createNameMissing:
msg: Please **specify the filename** of the resume to create.
pdfGeneration:
msg: PDF generation failed. Make sure wkhtmltopdf is installed and accessible from your path.
invalid:
msg: Validation failed and the --assert option was specified.
invalidFormat:
msg: The **%s** theme doesn't support the **%s** format.
notOnPath:
msg: %s wasn't found on your system path or is inaccessible. PDF not generated.
readError:
msg: Reading **???** resume: **%s**
parseError:
msg:
- Invalid or corrupt JSON on line %s column %s.
- Invalid or corrupt JSON on line %s.
- Invalid or corrupt JSON.
invalidHelperUse:
msg: "**Warning**: Incorrect use of the **%s** theme helper."
fileSaveError:
msg: An error occurred while writing %s to disk: %s.
mixedMerge:
msg: "**Warning:** merging mixed resume types. Errors may occur."
invokeTemplate:
msg: "An error occurred during template invocation."
compileTemplate:
msg: "An error occurred during template compilation."
themeLoad:
msg: "Applying **%s** theme (? formats)"
invalidParamCount:
msg: "Invalid number of parameters. Expected: **%s**."
missingParam:
msg: The '**%s**' parameter was needed but not supplied.
createError:
msg: Failed to create **'%s'**.
exiting:
msg: Exiting with status code **%s**.
validateError:
msg: "An error occurred during validation:\n%s"
invalidOptionsFile:
msg:
- "The specified options file is invalid:\n"
- "\nMake sure the options file contains valid JSON."
optionsFileNotFound:
msg: "The specified options file is missing or inaccessible."
unknownSchema:
msg:
- "Unknown resume schema. Did you specify a valid FRESH or JRS resume?"
- |
At a minimum, a FRESH resume must include a "name" field and a "meta"
property.
"name": "John Doe",
"meta": {
"format": "FRESH@0.1.0"
}
JRS-format resumes must include a "basics" section with a "name":
"basics": {
"name": "John Doe"
}
themeHelperLoad:
msg: >-
An error occurred while attempting to load the '%s' theme helper. Is the
theme correctly installed?
dummy: dontcare
invalidSchemaVersion:
msg: "'%s' is not recognized as a valid schema version."

204
src/cli/out.js Normal file
View File

@ -0,0 +1,204 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Output routines for HackMyResume.
@license MIT. See LICENSE.md for details.
@module cli/out
*/
const chalk = require('chalk');
const HME = require('../core/event-codes');
const _ = require('underscore');
const M2C = require('../utils/md2chalk.js');
const PATH = require('path');
const FS = require('fs');
const EXTEND = require('extend');
const HANDLEBARS = require('handlebars');
const YAML = require('yamljs');
let printf = require('printf');
const pad = require('string-padding');
const dbgStyle = 'cyan';
/** A stateful output module. All HMR console output handled here. */
class OutputHandler {
constructor( opts ) {
this.init(opts);
}
init(opts) {
this.opts = EXTEND( true, this.opts || { }, opts );
this.msgs = YAML.load(PATH.join( __dirname, 'msg.yml' )).events;
}
log() {
printf = require('printf');
const finished = printf.apply( printf, arguments );
return this.opts.silent || console.log( finished ); // eslint-disable-line no-console
}
do( evt ) {
const that = this;
const L = function() { return that.log.apply( that, arguments ); };
switch (evt.sub) {
case HME.begin:
return this.opts.debug &&
L( M2C( this.msgs.begin.msg, dbgStyle), evt.cmd.toUpperCase() );
//when HME.beforeCreate
//L( M2C( this.msgs.beforeCreate.msg, 'green' ), evt.fmt, evt.file )
//break;
case HME.afterCreate:
L( M2C( this.msgs.beforeCreate.msg, evt.isError ? 'red' : 'green' ), evt.fmt, evt.file );
break;
case HME.beforeTheme:
return this.opts.debug &&
L( M2C( this.msgs.beforeTheme.msg, dbgStyle), evt.theme.toUpperCase() );
case HME.afterParse:
return L( M2C( this.msgs.afterRead.msg, 'gray', 'white.dim'), evt.fmt.toUpperCase(), evt.file );
case HME.beforeMerge:
var msg = '';
evt.f.reverse().forEach(function( a, idx ) {
return msg += printf( (idx === 0 ? this.msgs.beforeMerge.msg[0] : this.msgs.beforeMerge.msg[1]), a.file );
}
, this);
return L( M2C(msg, (evt.mixed ? 'yellow' : 'gray'), 'white.dim') );
case HME.applyTheme:
this.theme = evt.theme;
var numFormats = Object.keys( evt.theme.formats ).length;
return L( M2C(this.msgs.applyTheme.msg,
evt.status === 'error' ? 'red' : 'gray',
evt.status === 'error' ? 'bold' : 'white.dim'),
evt.theme.name.toUpperCase(),
numFormats, numFormats === 1 ? '' : 's' );
case HME.end:
if (evt.cmd === 'build') {
const themeName = this.theme.name.toUpperCase();
if (this.opts.tips && (this.theme.message || this.theme.render)) {
if (this.theme.message) {
L( M2C( this.msgs.afterBuild.msg[0], 'cyan' ), themeName );
return L( M2C( this.theme.message, 'white' ));
} else if (this.theme.render) {
L( M2C( this.msgs.afterBuild.msg[0], 'cyan'), themeName);
return L( M2C( this.msgs.afterBuild.msg[1], 'white'));
}
}
}
break;
case HME.afterGenerate:
var suffix = '';
if (evt.fmt === 'pdf') {
if (this.opts.pdf) {
if (this.opts.pdf !== 'none') {
suffix = printf( M2C( this.msgs.afterGenerate.msg[0], evt.error ? 'red' : 'green' ), this.opts.pdf );
} else {
L( M2C( this.msgs.afterGenerate.msg[1], 'gray' ), evt.fmt.toUpperCase(), evt.file );
return;
}
}
}
return L( M2C( this.msgs.afterGenerate.msg[2] + suffix, evt.error ? 'red' : 'green' ),
pad( evt.fmt.toUpperCase(),4,null,pad.RIGHT ),
PATH.relative( process.cwd(), evt.file ) );
case HME.beforeAnalyze:
return L( M2C( this.msgs.beforeAnalyze.msg, 'green' ), evt.fmt, evt.file);
case HME.afterAnalyze:
var { info } = evt;
var rawTpl = FS.readFileSync( PATH.join( __dirname, 'analyze.hbs' ), 'utf8');
HANDLEBARS.registerHelper( require('../helpers/console-helpers') );
var template = HANDLEBARS.compile(rawTpl, { strict: false, assumeObjects: false });
var tot = 0;
info.keywords.forEach(g => tot += g.count);
info.keywords.totalKeywords = tot;
var output = template( info );
return this.log( chalk.cyan(output) );
case HME.beforeConvert:
return L( M2C( this.msgs.beforeConvert.msg, evt.error ? 'red' : 'green' ),
evt.srcFile, evt.srcFmt, evt.dstFile, evt.dstFmt
);
case HME.afterInlineConvert:
return L( M2C( this.msgs.afterInlineConvert.msg, 'gray', 'white.dim' ),
evt.file, evt.fmt );
case HME.afterValidate:
var style = 'red';
var adj = '';
var msgs = this.msgs.afterValidate.msg;
switch (evt.status) {
case 'valid': style = 'green'; adj = msgs[1]; break;
case 'invalid': style = 'yellow'; adj = msgs[2]; break;
case 'broken': style = 'red'; adj = msgs[3]; break;
case 'missing': style = 'red'; adj = msgs[4]; break;
case 'unknown': style = 'red'; adj = msgs[5]; break;
}
evt.schema = evt.schema.replace('jars','JSON Resume').toUpperCase();
L(M2C( msgs[0], 'white' ) + chalk[style].bold(adj), evt.file, evt.schema);
if (evt.violations) {
_.each(evt.violations, function(err) {
L( chalk.yellow.bold('--> ') +
chalk.yellow(err.field.replace('data.','resume.').toUpperCase() +
' ' + err.message));
}
, this);
}
return;
case HME.afterPeek:
var sty = evt.error ? 'red' : ( evt.target !== undefined ? 'green' : 'yellow' );
// "Peeking at 'someKey' in 'someFile'."
if (evt.requested) {
L(M2C(this.msgs.beforePeek.msg[0], sty), evt.requested, evt.file);
} else {
L(M2C(this.msgs.beforePeek.msg[1], sty), evt.file);
}
// If the key was present, print it
if ((evt.target !== undefined) && !evt.error) {
// eslint-disable-next-line no-console
return console.dir( evt.target, { depth: null, colors: true } );
// If the key was not present, but no error occurred, print it
} else if (!evt.error) {
return L(M2C( this.msgs.afterPeek.msg, 'yellow'), evt.requested, evt.file);
} else if (evt.error) {
return L(chalk.red( evt.error.inner.inner ));
}
break;
}
}
}
module.exports = OutputHandler;

View File

@ -0,0 +1,18 @@
/*
Event code definitions.
@module core/default-formats
@license MIT. See LICENSE.md for details.
*/
/** Supported resume formats. */
module.exports = [
{ name: 'html', ext: 'html', gen: new (require('../generators/html-generator'))() },
{ name: 'txt', ext: 'txt', gen: new (require('../generators/text-generator'))() },
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new (require('../generators/word-generator'))() },
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new (require('../generators/html-pdf-cli-generator'))() },
{ name: 'png', ext: 'png', fmt: 'html', is: false, gen: new (require('../generators/html-png-generator'))() },
{ name: 'md', ext: 'md', fmt: 'txt', gen: new (require('../generators/markdown-generator'))() },
{ name: 'json', ext: 'json', gen: new (require('../generators/json-generator'))() },
{ name: 'yml', ext: 'yml', fmt: 'yml', gen: new (require('../generators/json-yaml-generator'))() },
{ name: 'latex', ext: 'tex', fmt: 'latex', gen: new (require('../generators/latex-generator'))() }
];

View File

@ -0,0 +1,15 @@
/*
Event code definitions.
@module core/default-options
@license MIT. See LICENSE.md for details.
*/
module.exports = {
theme: 'modern',
prettify: { // ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2,
unformatted: ['em','strong'],
max_char: 80
} // ← See lib/html.js in above-linked repo
};
// wrap_line_length: 120, ← Don't use this

77
src/core/empty-jrs.json Normal file
View File

@ -0,0 +1,77 @@
{
"basics": {
"name": "",
"label": "",
"picture": "",
"email": "",
"phone": "",
"degree": "",
"website": "",
"summary": "",
"location": {
"address": "",
"postalCode": "",
"city": "",
"countryCode": "",
"region": ""
},
"profiles": [{
"network": "",
"username": "",
"url": ""
}]
},
"work": [{
"company": "",
"position": "",
"website": "",
"startDate": "",
"endDate": "",
"summary": "",
"highlights": [
""
]
}],
"awards": [{
"title": "",
"date": "",
"awarder": "",
"summary": ""
}],
"education": [{
"institution": "",
"area": "",
"studyType": "",
"startDate": "",
"endDate": "",
"gpa": "",
"courses": [ "" ]
}],
"publications": [{
"name": "",
"publisher": "",
"releaseDate": "",
"website": "",
"summary": ""
}],
"volunteer": [{
"organization": "",
"position": "",
"website": "",
"startDate": "",
"endDate": "",
"summary": "",
"highlights": [ "" ]
}],
"skills": [{
"name": "",
"level": "",
"keywords": [""]
}]
}

39
src/core/event-codes.js Normal file
View File

@ -0,0 +1,39 @@
/*
Event code definitions.
@module core/event-codes
@license MIT. See LICENSE.md for details.
*/
module.exports = {
error: -1,
success: 0,
begin: 1,
end: 2,
beforeRead: 3,
afterRead: 4,
beforeCreate: 5,
afterCreate: 6,
beforeTheme: 7,
afterTheme: 8,
beforeMerge: 9,
afterMerge: 10,
beforeGenerate: 11,
afterGenerate: 12,
beforeAnalyze: 13,
afterAnalyze: 14,
beforeConvert: 15,
afterConvert: 16,
verifyOutputs: 17,
beforeParse: 18,
afterParse: 19,
beforePeek: 20,
afterPeek: 21,
beforeInlineConvert: 22,
afterInlineConvert: 23,
beforeValidate: 24,
afterValidate: 25,
beforeWrite: 26,
afterWrite: 27,
applyTheme: 28
};

95
src/core/fluent-date.js Normal file
View File

@ -0,0 +1,95 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS104: Avoid inline assignments
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
The HackMyResume date representation.
@license MIT. See LICENSE.md for details.
@module core/fluent-date
*/
const moment = require('moment');
require('../utils/string');
/**
Create a FluentDate from a string or Moment date object. There are a few date
formats to be aware of here.
1. The words "Present" and "Now", referring to the current date
2. The default "YYYY-MM-DD" format used in JSON Resume ("2015-02-10")
3. Year-and-month only ("2015-04")
4. Year-only "YYYY" ("2015")
5. The friendly HackMyResume "mmm YYYY" format ("Mar 2015" or "Dec 2008")
6. Empty dates ("", " ")
7. Any other date format that Moment.js can parse from
Note: Moment can transparently parse all or most of these, without requiring us
to specify a date format...but for maximum parsing safety and to avoid Moment
deprecation warnings, it's recommended to either a) explicitly specify the date
format or b) use an ISO format. For clarity, we handle these cases explicitly.
@class FluentDate
*/
class FluentDate {
constructor(dt) {
this.rep = this.fmt(dt);
}
static isCurrent(dt) {
return !dt || (String.is(dt) && /^(present|now|current)$/.test(dt));
}
}
const months = {};
const abbr = {};
moment.months().forEach((m,idx) => months[m.toLowerCase()] = idx+1);
moment.monthsShort().forEach((m,idx) => abbr[m.toLowerCase()]=idx+1);
abbr.sept = 9;
module.exports = FluentDate;
FluentDate.fmt = function( dt, throws ) {
throws = ((throws === undefined) || (throws === null)) || throws;
if ((typeof dt === 'string') || dt instanceof String) {
dt = dt.toLowerCase().trim();
if (/^(present|now|current)$/.test(dt)) { // "Present", "Now"
return moment();
} else if (/^\D+\s+\d{4}$/.test(dt)) { // "Mar 2015"
let left;
const parts = dt.split(' ');
const month = (months[parts[0]] || abbr[parts[0]]);
const temp = parts[1] + '-' + ((left = month < 10) != null ? left : `0${{month : month.toString()}}`);
return moment(temp, 'YYYY-MM');
} else if (/^\d{4}-\d{1,2}$/.test(dt)) { // "2015-03", "1998-4"
return moment(dt, 'YYYY-MM');
} else if (/^\s*\d{4}\s*$/.test(dt)) { // "2015"
return moment(dt, 'YYYY');
} else if (/^\s*$/.test(dt)) { // "", " "
return moment();
} else {
const mt = moment(dt);
if (mt.isValid()) {
return mt;
}
if (throws) {
throw 'Invalid date format encountered.';
}
return null;
}
} else {
if (!dt) {
return moment();
} else if (dt.isValid && dt.isValid()) {
return dt;
}
if (throws) {
throw 'Unknown date object encountered.';
}
return null;
}
};

481
src/core/fresh-resume.js Normal file
View File

@ -0,0 +1,481 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the FRESHResume class.
@license MIT. See LICENSE.md for details.
@module core/fresh-resume
*/
const FS = require('fs');
const extend = require('extend');
let validator = require('is-my-json-valid');
const _ = require('underscore');
const __ = require('lodash');
const XML = require('xml-escape');
const MD = require('marked');
const CONVERTER = require('fresh-jrs-converter');
const JRSResume = require('./jrs-resume');
/**
A FRESH resume or CV. FRESH resumes are backed by JSON, and each FreshResume
object is an instantiation of that JSON decorated with utility methods.
@constructor
*/
class FreshResume {// extends AbstractResume
/** Initialize the the FreshResume from JSON string data. */
parse( stringData, opts ) {
this.imp = this.imp != null ? this.imp : {raw: stringData};
return this.parseJSON(JSON.parse( stringData ), opts);
}
/**
Initialize the FreshResume from JSON.
Open and parse the specified FRESH resume. Merge the JSON object model onto
this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
@param rep {Object} The raw JSON representation.
@param opts {Object} Resume loading and parsing options.
{
date: Perform safe date conversion.
sort: Sort resume items by date.
compute: Prepare computed resume totals.
}
*/
parseJSON( rep, opts ) {
let scrubbed;
if (opts && opts.privatize) {
// Ignore any element with the 'ignore: true' or 'private: true' designator.
const scrubber = require('../utils/resume-scrubber');
var ret = scrubber.scrubResume(rep, opts);
scrubbed = ret.scrubbed;
}
// Now apply the resume representation onto this object
extend(true, this, opts && opts.privatize ? scrubbed : rep);
// If the resume has already been processed, then we are being called from
// the .dupe method, and there's no need to do any post processing
if (!(this.imp != null ? this.imp.processed : undefined)) {
// Set up metadata TODO: Clean up metadata on the object model.
opts = opts || { };
if ((opts.imp === undefined) || opts.imp) {
this.imp = this.imp || { };
this.imp.title = (opts.title || this.imp.title) || this.name;
if (!this.imp.raw) {
this.imp.raw = JSON.stringify(rep);
}
}
this.imp.processed = true;
// Parse dates, sort dates, and calculate computed values
((opts.date === undefined) || opts.date) && _parseDates.call( this );
((opts.sort === undefined) || opts.sort) && this.sort();
((opts.compute === undefined) || opts.compute) && (this.computed = {
numYears: this.duration(),
keywords: this.keywords()
});
}
return this;
}
/** Save the sheet to disk (for environments that have disk access). */
save( filename ) {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
return this;
}
/**
Save the sheet to disk in a specific format, either FRESH or JSON Resume.
*/
saveAs( filename, format ) {
// If format isn't specified, default to FRESH
const safeFormat = (format && format.trim()) || 'FRESH';
// Validate against the FRESH version regex
// freshVersionReg = require '../utils/fresh-version-regex'
// if (not freshVersionReg().test( safeFormat ))
// throw badVer: safeFormat
const parts = safeFormat.split('@');
if (parts[0] === 'FRESH') {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify(), 'utf8');
} else if (parts[0] === 'JRS') {
const useEdgeSchema = parts.length > 1 ? parts[1] === '1' : false;
const newRep = CONVERTER.toJRS(this, {edge: useEdgeSchema});
FS.writeFileSync(filename, JRSResume.stringify( newRep ), 'utf8');
} else {
throw {badVer: safeFormat};
}
return this;
}
/**
Duplicate this FreshResume instance.
This method first extend()s this object onto an empty, creating a deep copy,
and then passes the result into a new FreshResume instance via .parseJSON.
We do it this way to create a true clone of the object without re-running any
of the associated processing.
*/
dupe() {
const jso = extend(true, { }, this);
const rnew = new FreshResume();
rnew.parseJSON(jso, { });
return rnew;
}
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way.
*/
stringify() { return FreshResume.stringify(this); }
/**
Create a copy of this resume in which all string fields have been run through
a transformation function (such as a Markdown filter or XML encoder).
TODO: Move this out of FRESHResume.
*/
transformStrings( filt, transformer ) {
const ret = this.dupe();
const trx = require('../utils/string-transformer');
return trx(ret, filt, transformer);
}
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
markdownify() {
const MDIN = txt => MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
const trx = function( key, val ) {
if (key === 'summary') {
return MD(val);
}
return MDIN(val);
};
return this.transformStrings(['skills','url','start','end','date'], trx);
}
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
xmlify() {
const trx = (key, val) => XML(val);
return this.transformStrings([], trx);
}
/** Return the resume format. */
format() { return 'FRESH'; }
/**
Return internal metadata. Create if it doesn't exist.
*/
i() { return this.imp = this.imp || { }; }
/**
Return a unique list of all skills declared in the resume.
*/
// TODO: Several problems here:
// 1) Confusing name. Easily confused with the keyword-inspector module, which
// parses resume body text looking for these same keywords. This should probably
// be renamed.
//
// 2) Doesn't bother trying to integrate skills.list with skills.sets if they
// happen to declare different skills, and if skills.sets declares ONE skill and
// skills.list declared 50, only 1 skill will be registered.
//
// 3) In the future, skill.sets should only be able to use skills declared in
// skills.list. That is, skills.list is the official record of a candidate's
// declared skills. skills.sets is just a way of grouping those into skillsets
// for easier consumption.
keywords() {
let flatSkills = [];
if (this.skills) {
if (this.skills.sets) {
flatSkills = this.skills.sets.map(sk => sk.skills).reduce( (a,b) => a.concat(b));
} else if (this.skills.list) {
flatSkills = flatSkills.concat( this.skills.list.map(sk => sk.name) );
}
flatSkills = _.uniq(flatSkills);
}
return flatSkills;
}
/**
Reset the sheet to an empty state. TODO: refactor/review
*/
clear( clearMeta ) {
clearMeta = ((clearMeta === undefined) && true) || clearMeta;
if (clearMeta) { delete this.imp; }
delete this.computed; // Don't use Object.keys() here
delete this.employment;
delete this.service;
delete this.education;
delete this.recognition;
delete this.reading;
delete this.writing;
delete this.interests;
delete this.skills;
return delete this.social;
}
/**
Get a safe count of the number of things in a section.
*/
count( obj ) {
if (!obj) { return 0; }
if (obj.history) { return obj.history.length; }
if (obj.sets) { return obj.sets.length; }
return obj.length || 0;
}
/** Add work experience to the sheet. */
add( moniker ) {
const defSheet = FreshResume.default();
const newObject =
defSheet[moniker].history
? extend( true, {}, defSheet[ moniker ].history[0] )
:
moniker === 'skills'
? extend( true, {}, defSheet.skills.sets[0] )
: extend( true, {}, defSheet[ moniker ][0] );
this[ moniker ] = this[ moniker ] || [];
if (this[ moniker ].history) {
this[ moniker ].history.push(newObject);
} else if (moniker === 'skills') {
this.skills.sets.push(newObject);
} else {
this[ moniker ].push(newObject);
}
return newObject;
}
/**
Determine if the sheet includes a specific social profile (eg, GitHub).
*/
hasProfile( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.some(this.social, p => p.network.trim().toLowerCase() === socialNetwork);
}
/** Return the specified network profile. */
getProfile( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.find(this.social, sn => sn.network.trim().toLowerCase() === socialNetwork);
}
/**
Return an array of profiles for the specified network, for when the user
has multiple eg. GitHub accounts.
*/
getProfiles( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.social && _.filter(this.social, sn => sn.network.trim().toLowerCase() === socialNetwork);
}
/** Determine if the sheet includes a specific skill. */
hasSkill( skill ) {
skill = skill.trim().toLowerCase();
return this.skills && _.some(this.skills, sk =>
sk.keywords && _.some(sk.keywords, kw => kw.trim().toLowerCase() === skill)
);
}
/** Validate the sheet against the FRESH Resume schema. */
isValid() {
const schemaObj = require('fresh-resume-schema');
validator = require('is-my-json-valid');
const validate = validator( schemaObj, { // See Note [1].
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
});
const ret = validate(this);
if (!ret) {
this.imp = this.imp || { };
this.imp.validationErrors = validate.errors;
}
return ret;
}
duration(unit) {
const inspector = require('../inspectors/duration-inspector');
return inspector.run(this, 'employment.history', 'start', 'end', unit);
}
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
sort() {
const byDateDesc = function(a,b) {
if (a.safe.start.isBefore(b.safe.start)) {
return 1;
} else { if (a.safe.start.isAfter(b.safe.start)) { return -1; } else { return 0; } }
};
const sortSection = function( key ) {
const ar = __.get(this, key);
if (ar && ar.length) {
const datedThings = ar.filter(o => o.start);
return datedThings.sort( byDateDesc );
}
};
sortSection('employment.history');
sortSection('education.history');
sortSection('service.history');
sortSection('projects');
return this.writing && this.writing.sort(function(a, b) {
if (a.safe.date.isBefore(b.safe.date)) {
return 1;
} else { return ( a.safe.date.isAfter(b.safe.date) && -1 ) || 0; }
});
}
}
/**
Get the default (starter) sheet.
*/
FreshResume.default = () => new FreshResume().parseJSON(require('fresh-resume-starter').fresh);
/**
Convert the supplied FreshResume to a JSON string, sanitizing meta-properties
along the way.
*/
FreshResume.stringify = function( obj ) {
const replacer = function( key,value ) { // Exclude these keys from stringification
const exKeys = ['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safe', 'result', 'isModified', 'htmlPreview', 'display_progress_bar'];
if (_.some( exKeys, val => key.trim() === val)) {
return undefined; } else { return value; }
};
return JSON.stringify(obj, replacer, 2);
};
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
*/
var _parseDates = function() {
const _fmt = require('./fluent-date').fmt;
const that = this;
// TODO: refactor recursion
var replaceDatesInObject = function( obj ) {
if (!obj) { return; }
if (Object.prototype.toString.call( obj ) === '[object Array]') {
obj.forEach(elem => replaceDatesInObject( elem ));
return;
} else if (typeof obj === 'object') {
if (obj._isAMomentObject || obj.safe) {
return;
}
Object.keys( obj ).forEach(key => replaceDatesInObject(obj[key]));
['start','end','date'].forEach(function(val) {
if ((obj[val] !== undefined) && (!obj.safe || !obj.safe[val])) {
obj.safe = obj.safe || { };
obj.safe[ val ] = _fmt(obj[val]);
if (obj[val] && (val === 'start') && !obj.end) {
obj.safe.end = _fmt('current');
return;
}
}
});
return;
}
};
Object.keys( this ).forEach(function(member) {
replaceDatesInObject(that[member]);
});
};
/** Export the Sheet function/ctor. */
module.exports = FreshResume;
// Note 1: Adjust default date validation to allow YYYY and YYYY-MM formats
// in addition to YYYY-MM-DD. The original regex:
//
// /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}$/
//

253
src/core/fresh-theme.js Normal file
View File

@ -0,0 +1,253 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the FRESHTheme class.
@module core/fresh-theme
@license MIT. See LICENSE.md for details.
*/
const FS = require('fs');
const _ = require('underscore');
const PATH = require('path');
const parsePath = require('parse-filepath');
const EXTEND = require('extend');
const HMSTATUS = require('./status-codes');
const loadSafeJson = require('../utils/safe-json-loader');
const READFILES = require('recursive-readdir-sync');
/* A representation of a FRESH theme asset.
@class FRESHTheme */
class FRESHTheme {
constructor() {
this.baseFolder = 'src';
}
/* Open and parse the specified theme. */
open( themeFolder ) {
this.folder = themeFolder;
// Set up a formats hash for the theme
let formatsHash = { };
// Load the theme
const themeFile = PATH.join(themeFolder, 'theme.json');
const themeInfo = loadSafeJson(themeFile);
if (themeInfo.ex) {
throw{
fluenterror:
themeInfo.ex.op === 'parse'
? HMSTATUS.parseError
: HMSTATUS.readError,
inner: themeInfo.ex.inner
};
}
// Move properties from the theme JSON file to the theme object
EXTEND(true, this, themeInfo.json);
// Check for an "inherits" entry in the theme JSON.
if (this.inherits) {
const cached = { };
_.each(this.inherits, function(th, key) {
// First, see if this is one of the predefined FRESH themes. There are
// only a handful of these, but they may change over time, so we need to
// query the official source of truth: the fresh-themes repository, which
// mounts the themes conveniently by name to the module object, and which
// is embedded locally inside the HackMyResume installation.
// TODO: merge this code with
let themePath;
const themesObj = require('fresh-themes');
if (_.has(themesObj.themes, th)) {
themePath = PATH.join(
parsePath( require.resolve('fresh-themes') ).dirname,
'/themes/',
th
);
} else {
const d = parsePath( th ).dirname;
themePath = PATH.join(d, th);
}
cached[ th ] = cached[th] || new FRESHTheme().open( themePath );
return formatsHash[ key ] = cached[ th ].getFormat( key );
});
}
// Load theme files
formatsHash = _load.call(this, formatsHash);
// Cache
this.formats = formatsHash;
// Set the official theme name
this.name = parsePath( this.folder ).name;
return this;
}
/* Determine if the theme supports the specified output format. */
hasFormat( fmt ) { return _.has(this.formats, fmt); }
/* Determine if the theme supports the specified output format. */
getFormat( fmt ) { return this.formats[ fmt ]; }
}
/* Load and parse theme source files. */
var _load = function(formatsHash) {
const that = this;
const tplFolder = PATH.join(this.folder, this.baseFolder);
// Iterate over all files in the theme folder, producing an array, fmts,
// containing info for each file. While we're doing that, also build up
// the formatsHash object.
const fmts = READFILES(tplFolder).map(function(absPath) {
return _loadOne.call(this, absPath, formatsHash, tplFolder);
}
, this);
// Now, get all the CSS files...
this.cssFiles = fmts.filter(fmt => fmt && (fmt.ext === 'css'));
// For each CSS file, get its corresponding HTML file. It's possible that
// a theme can have a CSS file but *no* HTML file, as when a theme author
// creates a pure CSS override of an existing theme.
this.cssFiles.forEach(function(cssf) {
const idx = _.findIndex(fmts, fmt => fmt && (fmt.pre === cssf.pre) && (fmt.ext === 'html'));
cssf.major = false;
if (idx > -1) {
fmts[ idx ].css = cssf.data;
return fmts[ idx ].cssPath = cssf.path;
} else {
if (that.inherits) {
// Found a CSS file without an HTML file in a theme that inherits
// from another theme. This is the override CSS file.
return that.overrides = { file: cssf.path, data: cssf.data };
}
}});
// Now, save all the javascript file paths to a theme property.
const jsFiles = fmts.filter(fmt => fmt && (fmt.ext === 'js'));
this.jsFiles = jsFiles.map(jsf => jsf['path']);
return formatsHash;
};
/* Load a single theme file. */
var _loadOne = function( absPath, formatsHash, tplFolder ) {
const pathInfo = parsePath(absPath);
if (pathInfo.basename.toLowerCase() === 'theme.json') { return; }
const absPathSafe = absPath.trim().toLowerCase();
let outFmt = '';
let act = 'copy';
let isPrimary = false;
// If this is an "explicit" theme, all files of importance are specified in
// the "transform" section of the theme.json file.
if (this.explicit) {
outFmt = _.find(Object.keys( this.formats ), function( fmtKey ) {
const fmtVal = this.formats[ fmtKey ];
return _.some(fmtVal.transform, function(fpath) {
const absPathB = PATH.join( this.folder, fpath ).trim().toLowerCase();
return absPathB === absPathSafe;
}
, this);
}
, this);
if (outFmt) { act = 'transform'; }
}
if (!outFmt) {
// If this file lives in a specific format folder within the theme,
// such as "/latex" or "/html", then that format is the implicit output
// format for all files within the folder
const portion = pathInfo.dirname.replace(tplFolder,'');
if (portion && portion.trim()) {
if (portion[1] === '_') { return; }
const reg = /^(?:\/|\\)(html|latex|doc|pdf|png|partials)(?:\/|\\)?/ig;
const res = reg.exec( portion );
if (res) {
if (res[1] !== 'partials') {
outFmt = res[1];
if (!this.explicit) { act = 'transform'; }
} else {
this.partials = this.partials || [];
this.partials.push( { name: pathInfo.name, path: absPath } );
return null;
}
}
}
}
// Otherwise, the output format is inferred from the filename, as in
// compact-[outputformat].[extension], for ex, compact-pdf.html
if (!outFmt) {
const idx = pathInfo.name.lastIndexOf('-');
outFmt = idx === -1 ? pathInfo.name : pathInfo.name.substr(idx+1);
if (!this.explicit) { act = 'transform'; }
const defFormats = require('./default-formats');
isPrimary = _.some(defFormats, form => (form.name === outFmt) && (pathInfo.extname !== '.css'));
}
// Make sure we have a valid formatsHash
formatsHash[ outFmt ] = formatsHash[outFmt] || {
outFormat: outFmt,
files: []
};
// Move symlink descriptions from theme.json to the format
if (__guard__(this.formats != null ? this.formats[outFmt ] : undefined, x => x.symLinks)) {
formatsHash[ outFmt ].symLinks = this.formats[ outFmt ].symLinks;
}
// Create the file representation object
const obj = {
action: act,
primary: isPrimary,
path: absPath,
orgPath: PATH.relative(tplFolder, absPath),
ext: pathInfo.extname.slice(1),
title: friendlyName(outFmt),
pre: outFmt,
// outFormat: outFmt || pathInfo.name,
data: FS.readFileSync(absPath, 'utf8'),
css: null
};
// Add this file to the list of files for this format type.
formatsHash[ outFmt ].files.push( obj );
return obj;
};
/* Return a more friendly name for certain formats. */
var friendlyName = function( val ) {
val = (val && val.trim().toLowerCase()) || '';
const friendly = { yml: 'yaml', md: 'markdown', txt: 'text' };
return friendly[val] || val;
};
module.exports = FRESHTheme;
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

348
src/core/jrs-resume.js Normal file
View File

@ -0,0 +1,348 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the JRSResume class.
@license MIT. See LICENSE.md for details.
@module core/jrs-resume
*/
const FS = require('fs');
const extend = require('extend');
let validator = require('is-my-json-valid');
const _ = require('underscore');
const PATH = require('path');
const CONVERTER = require('fresh-jrs-converter');
/**
A JRS resume or CV. JRS resumes are backed by JSON, and each JRSResume object
is an instantiation of that JSON decorated with utility methods.
@class JRSResume
*/
class JRSResume {
static initClass() {
/** Reset the sheet to an empty state. */
// clear = function( clearMeta ) {
// clearMeta = ((clearMeta === undefined) && true) || clearMeta;
// if (clearMeta) { delete this.imp; }
// delete this.basics.computed; // Don't use Object.keys() here
// delete this.work;
// delete this.volunteer;
// delete this.education;
// delete this.awards;
// delete this.publications;
// delete this.interests;
// delete this.skills;
// return delete this.basics.profiles;
// };
// extends AbstractResume
}
/** Initialize the the JSResume from string. */
parse( stringData, opts ) {
this.imp = this.imp != null ? this.imp : {raw: stringData};
return this.parseJSON(JSON.parse( stringData ), opts);
}
/**
Initialize the JRSResume object from JSON.
Open and parse the specified JRS resume. Merge the JSON object model onto
this Sheet instance with extend() and convert sheet dates to a safe &
consistent format. Then sort each section by startDate descending.
@param rep {Object} The raw JSON representation.
@param opts {Object} Resume loading and parsing options.
{
date: Perform safe date conversion.
sort: Sort resume items by date.
compute: Prepare computed resume totals.
}
*/
parseJSON( rep, opts ) {
let scrubbed;
opts = opts || { };
if (opts.privatize) {
const scrubber = require('../utils/resume-scrubber');
// Ignore any element with the 'ignore: true' or 'private: true' designator.
var ret = scrubber.scrubResume(rep, opts);
scrubbed = ret.scrubbed;
}
// Extend resume properties onto ourself.
extend(true, this, opts.privatize ? scrubbed : rep);
// Set up metadata
if (!(this.imp != null ? this.imp.processed : undefined)) {
// Set up metadata TODO: Clean up metadata on the object model.
opts = opts || { };
if ((opts.imp === undefined) || opts.imp) {
this.imp = this.imp || { };
this.imp.title = (opts.title || this.imp.title) || this.basics.name;
if (!this.imp.raw) {
this.imp.raw = JSON.stringify(rep);
}
}
this.imp.processed = true;
}
// Parse dates, sort dates, and calculate computed values
((opts.date === undefined) || opts.date) && _parseDates.call( this );
((opts.sort === undefined) || opts.sort) && this.sort();
if ((opts.compute === undefined) || opts.compute) {
this.basics.computed = {
numYears: this.duration(),
keywords: this.keywords()
};
}
return this;
}
/** Save the sheet to disk (for environments that have disk access). */
save( filename ) {
this.imp.file = filename || this.imp.file;
FS.writeFileSync(this.imp.file, this.stringify( this ), 'utf8');
return this;
}
/** Save the sheet to disk in a specific format, either FRESH or JRS. */
saveAs( filename, format ) {
if (format === 'JRS') {
this.imp.file = filename || this.imp.file;
FS.writeFileSync( this.imp.file, this.stringify(), 'utf8' );
} else {
const newRep = CONVERTER.toFRESH(this);
const stringRep = CONVERTER.toSTRING(newRep);
FS.writeFileSync(filename, stringRep, 'utf8');
}
return this;
}
/** Return the resume format. */
format() { return 'JRS'; }
stringify() { return JRSResume.stringify( this ); }
/** Return a unique list of all keywords across all skills. */
keywords() {
let flatSkills = [];
if (this.skills && this.skills.length) {
this.skills.forEach( s => flatSkills = _.union(flatSkills, s.keywords));
}
return flatSkills;
}
/**
Return internal metadata. Create if it doesn't exist.
JSON Resume v0.0.0 doesn't allow additional properties at the root level,
so tuck this into the .basic sub-object.
*/
i() {
return this.imp = this.imp != null ? this.imp : { };
}
/** Add work experience to the sheet. */
add( moniker ) {
const defSheet = JRSResume.default();
const newObject = extend( true, {}, defSheet[ moniker ][0] );
this[ moniker ] = this[ moniker ] || [];
this[ moniker ].push( newObject );
return newObject;
}
/** Determine if the sheet includes a specific social profile (eg, GitHub). */
hasProfile( socialNetwork ) {
socialNetwork = socialNetwork.trim().toLowerCase();
return this.basics.profiles && _.some(this.basics.profiles, p => p.network.trim().toLowerCase() === socialNetwork);
}
/** Determine if the sheet includes a specific skill. */
hasSkill( skill ) {
skill = skill.trim().toLowerCase();
return this.skills && _.some(this.skills, sk =>
sk.keywords && _.some(sk.keywords, kw => kw.trim().toLowerCase() === skill)
);
}
/** Validate the sheet against the JSON Resume schema. */
isValid( ) { // TODO: ↓ fix this path ↓
const schema = FS.readFileSync(PATH.join( __dirname, 'resume.json' ), 'utf8');
const schemaObj = JSON.parse(schema);
validator = require('is-my-json-valid');
const validate = validator( schemaObj, { // Note [1]
formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }
});
const temp = this.imp;
delete this.imp;
const ret = validate(this);
this.imp = temp;
if (!ret) {
this.imp = this.imp || { };
this.imp.validationErrors = validate.errors;
}
return ret;
}
duration(unit) {
const inspector = require('../inspectors/duration-inspector');
return inspector.run(this, 'work', 'startDate', 'endDate', unit);
}
/**
Sort dated things on the sheet by start date descending. Assumes that dates
on the sheet have been processed with _parseDates().
*/
sort( ) {
const byDateDesc = function(a,b) {
if (a.safeStartDate.isBefore(b.safeStartDate)) {
return 1;
} else { return ( a.safeStartDate.isAfter(b.safeStartDate) && -1 ) || 0; }
};
this.work && this.work.sort(byDateDesc);
this.education && this.education.sort(byDateDesc);
this.volunteer && this.volunteer.sort(byDateDesc);
this.awards && this.awards.sort(function(a, b) {
if (a.safeDate.isBefore(b.safeDate)) {
return 1;
} else { return (a.safeDate.isAfter(b.safeDate) && -1 ) || 0; }
});
return this.publications && this.publications.sort(function(a, b) {
if ( a.safeReleaseDate.isBefore(b.safeReleaseDate) ) {
return 1;
} else { return ( a.safeReleaseDate.isAfter(b.safeReleaseDate) && -1 ) || 0; }
});
}
dupe() {
const rnew = new JRSResume();
rnew.parse(this.stringify(), { });
return rnew;
}
/**
Create a copy of this resume in which all fields have been interpreted as
Markdown.
*/
harden() {
const ret = this.dupe();
const HD = txt => `@@@@~${txt}~@@@@`;
// const HDIN = txt =>
// //return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '');
// HD(txt)
// ;
const transformer = require('../utils/string-transformer');
return transformer(ret,
[ 'skills','url','website','startDate','endDate', 'releaseDate', 'date',
'phone','email','address','postalCode','city','country','region',
'safeStartDate','safeEndDate' ],
(key, val) => HD(val));
}
}
JRSResume.initClass();
/** Get the default (empty) sheet. */
JRSResume.default = () => new JRSResume().parseJSON(require('fresh-resume-starter').jrs);
/**
Convert this object to a JSON string, sanitizing meta-properties along the
way. Don't override .toString().
*/
JRSResume.stringify = function( obj ) {
const replacer = function( key,value ) { // Exclude these keys from stringification
const temp = _.some(['imp', 'warnings', 'computed', 'filt', 'ctrl', 'index',
'safeStartDate', 'safeEndDate', 'safeDate', 'safeReleaseDate', 'result',
'isModified', 'htmlPreview', 'display_progress_bar'],
val => key.trim() === val);
if (temp) { return undefined; } else { return value; }
};
return JSON.stringify(obj, replacer, 2);
};
/**
Convert human-friendly dates into formal Moment.js dates for all collections.
We don't want to lose the raw textual date as entered by the user, so we store
the Moment-ified date as a separate property with a prefix of .safe. For ex:
job.startDate is the date as entered by the user. job.safeStartDate is the
parsed Moment.js date that we actually use in processing.
*/
var _parseDates = function() {
const _fmt = require('./fluent-date').fmt;
this.work && this.work.forEach(function(job) {
job.safeStartDate = _fmt( job.startDate );
return job.safeEndDate = _fmt( job.endDate );
});
this.education && this.education.forEach(function(edu) {
edu.safeStartDate = _fmt( edu.startDate );
return edu.safeEndDate = _fmt( edu.endDate );
});
this.volunteer && this.volunteer.forEach(function(vol) {
vol.safeStartDate = _fmt( vol.startDate );
return vol.safeEndDate = _fmt( vol.endDate );
});
this.awards && this.awards.forEach(awd => awd.safeDate = _fmt( awd.date ));
return this.publications && this.publications.forEach(pub => pub.safeReleaseDate = _fmt( pub.releaseDate ));
};
/**
Export the JRSResume class.
*/
module.exports = JRSResume;

96
src/core/jrs-theme.js Normal file
View File

@ -0,0 +1,96 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the JRSTheme class.
@module core/jrs-theme
@license MIT. See LICENSE.MD for details.
*/
const _ = require('underscore');
const PATH = require('path');
const pathExists = require('path-exists').sync;
const errors = require('./status-codes');
/**
The JRSTheme class is a representation of a JSON Resume theme asset.
@class JRSTheme
*/
class JRSTheme {
/**
Open and parse the specified JRS theme.
@method open
*/
open( thFolder ) {
this.folder = thFolder;
//const pathInfo = parsePath(thFolder);
// Open and parse the theme's package.json file
const pkgJsonPath = PATH.join(thFolder, 'package.json');
if (pathExists(pkgJsonPath)) {
const thApi = require(thFolder); // Requiring the folder yields whatever the package.json's "main" is set to
const thPkg = require(pkgJsonPath); // Get the package.json as JSON
this.name = thPkg.name;
this.render = (thApi && thApi.render) || undefined;
this.engine = 'jrs';
// Create theme formats (HTML and PDF). Just add the bare minimum mix of
// properties necessary to allow JSON Resume themes to share a rendering
// path with FRESH themes.
this.formats = {
html: {
outFormat: 'html',
files: [{
action: 'transform',
render: this.render,
primary: true,
ext: 'html',
css: null
}]
},
pdf: {
outFormat: 'pdf',
files: [{
action: 'transform',
render: this.render,
primary: true,
ext: 'pdf',
css: null
}]
}
};
} else {
throw {fluenterror: errors.missingPackageJSON};
}
return this;
}
/**
Determine if the theme supports the output format.
@method hasFormat
*/
hasFormat( fmt ) { return _.has(this.formats, fmt); }
/**
Return the requested output format.
@method getFormat
*/
getFormat( fmt ) { return this.formats[ fmt ]; }
}
module.exports = JRSTheme;

127
src/core/resume-factory.js Normal file
View File

@ -0,0 +1,127 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the ResumeFactory class.
@license MIT. See LICENSE.md for details.
@module core/resume-factory
*/
const FS = require('fs');
const HMS = require('./status-codes');
const HME = require('./event-codes');
const ResumeConverter = require('fresh-jrs-converter');
const resumeDetect = require('../utils/resume-detector');
require('string.prototype.startswith');
/**
A simple factory class for FRESH and JSON Resumes.
@class ResumeFactory
*/
module.exports = {
/**
Load one or more resumes from disk.
@param {Object} opts An options object with settings for the factory as well
as passthrough settings for FRESHResume or JRSResume. Structure:
{
format: 'FRESH', // Format to open as. ('FRESH', 'JRS', null)
objectify: true, // FRESH/JRSResume or raw JSON?
inner: { // Passthru options for FRESH/JRSResume
sort: false
}
}
*/
load( sources, opts, emitter ) {
return sources.map( function(src) {
return this.loadOne( src, opts, emitter );
}
, this);
},
/** Load a single resume from disk. */
loadOne( src, opts, emitter ) {
let toFormat = opts.format; // Can be null
// Get the destination format. Can be 'fresh', 'jrs', or null/undefined.
toFormat && (toFormat = toFormat.toLowerCase().trim());
// Load and parse the resume JSON
const info = _parse(src, opts, emitter);
if (info.fluenterror) { return info; }
// Determine the resume format: FRESH or JRS
let { json } = info;
const orgFormat = resumeDetect(json);
if (orgFormat === 'unk') {
info.fluenterror = HMS.unknownSchema;
return info;
}
// Convert between formats if necessary
if (toFormat && ( orgFormat !== toFormat )) {
json = ResumeConverter[ `to${toFormat.toUpperCase()}` ](json);
}
// Objectify the resume, that is, convert it from JSON to a FRESHResume
// or JRSResume object.
let rez = null;
if (opts.objectify) {
const reqLib = `../core/${toFormat || orgFormat}-resume`;
const ResumeClass = require(reqLib);
rez = new ResumeClass().parseJSON( json, opts.inner );
rez.i().file = src;
}
return {
file: src,
json: info.json,
rez
};
}
};
var _parse = function( fileName, opts, eve ) {
let rawData = null;
try {
// Read the file
eve && eve.stat( HME.beforeRead, { file: fileName });
rawData = FS.readFileSync( fileName, 'utf8' );
eve && eve.stat( HME.afterRead, { file: fileName, data: rawData });
// Parse the file
eve && eve.stat(HME.beforeParse, { data: rawData });
const ret = { json: JSON.parse( rawData ) };
const orgFormat =
ret.json.meta && ret.json.meta.format && ret.json.meta.format.startsWith('FRESH@')
? 'fresh' : 'jrs';
eve && eve.stat(HME.afterParse, { file: fileName, data: ret.json, fmt: orgFormat });
return ret;
} catch (err) {
// Can be ENOENT, EACCES, SyntaxError, etc.
return {
fluenterror: rawData ? HMS.parseError : HMS.readError,
inner: err,
raw: rawData,
file: fileName
};
}
};

380
src/core/resume.json Normal file
View File

@ -0,0 +1,380 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Resume Schema",
"type": "object",
"additionalProperties": false,
"properties": {
"basics": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string"
},
"label": {
"type": "string",
"description": "e.g. Web Developer"
},
"picture": {
"type": "string",
"description": "URL (as per RFC 3986) to a picture in JPEG or PNG format"
},
"email": {
"type": "string",
"description": "e.g. thomas@gmail.com",
"format": "email"
},
"phone": {
"type": "string",
"description": "Phone numbers are stored as strings so use any format you like, e.g. 712-117-2923"
},
"website": {
"type": "string",
"description": "URL (as per RFC 3986) to your website, e.g. personal homepage",
"format": "uri"
},
"summary": {
"type": "string",
"description": "Write a short 2-3 sentence biography about yourself"
},
"location": {
"type": "object",
"additionalProperties": true,
"properties": {
"address": {
"type": "string",
"description": "To add multiple address lines, use \n. For example, 1234 Glücklichkeit Straße\nHinterhaus 5. Etage li."
},
"postalCode": {
"type": "string"
},
"city": {
"type": "string"
},
"countryCode": {
"type": "string",
"description": "code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN"
},
"region": {
"type": "string",
"description": "The general region where you live. Can be a US state, or a province, for instance."
}
}
},
"profiles": {
"type": "array",
"description": "Specify any number of social networks that you participate in",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"network": {
"type": "string",
"description": "e.g. Facebook or Twitter"
},
"username": {
"type": "string",
"description": "e.g. neutralthoughts"
},
"url": {
"type": "string",
"description": "e.g. http://twitter.com/neutralthoughts"
}
}
}
}
}
},
"work": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"company": {
"type": "string",
"description": "e.g. Facebook"
},
"position": {
"type": "string",
"description": "e.g. Software Engineer"
},
"website": {
"type": "string",
"description": "e.g. http://facebook.com",
"format": "uri"
},
"startDate": {
"type": "string",
"description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"summary": {
"type": "string",
"description": "Give an overview of your responsibilities at the company"
},
"highlights": {
"type": "array",
"description": "Specify multiple accomplishments",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
}
}
}
}
},
"volunteer": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"organization": {
"type": "string",
"description": "e.g. Facebook"
},
"position": {
"type": "string",
"description": "e.g. Software Engineer"
},
"website": {
"type": "string",
"description": "e.g. http://facebook.com",
"format": "uri"
},
"startDate": {
"type": "string",
"description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"summary": {
"type": "string",
"description": "Give an overview of your responsibilities at the company"
},
"highlights": {
"type": "array",
"description": "Specify multiple accomplishments",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
}
}
}
}
},
"education": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"institution": {
"type": "string",
"description": "e.g. Massachusetts Institute of Technology"
},
"area": {
"type": "string",
"description": "e.g. Arts"
},
"studyType": {
"type": "string",
"description": "e.g. Bachelor"
},
"startDate": {
"type": "string",
"description": "e.g. 2014-06-29",
"format": "date"
},
"endDate": {
"type": "string",
"description": "e.g. 2012-06-29",
"format": "date"
},
"gpa": {
"type": "string",
"description": "grade point average, e.g. 3.67/4.0"
},
"courses": {
"type": "array",
"description": "List notable courses/subjects",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. H1302 - Introduction to American history"
}
}
}
}
},
"awards": {
"type": "array",
"description": "Specify any awards you have received throughout your professional career",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"title": {
"type": "string",
"description": "e.g. One of the 100 greatest minds of the century"
},
"date": {
"type": "string",
"description": "e.g. 1989-06-12",
"format": "date"
},
"awarder": {
"type": "string",
"description": "e.g. Time Magazine"
},
"summary": {
"type": "string",
"description": "e.g. Received for my work with Quantum Physics"
}
}
}
},
"publications": {
"type": "array",
"description": "Specify your publications through your career",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. The World Wide Web"
},
"publisher": {
"type": "string",
"description": "e.g. IEEE, Computer Magazine"
},
"releaseDate": {
"type": "string",
"description": "e.g. 1990-08-01"
},
"website": {
"type": "string",
"description": "e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html"
},
"summary": {
"type": "string",
"description": "Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML."
}
}
}
},
"skills": {
"type": "array",
"description": "List out your professional skill-set",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Web Development"
},
"level": {
"type": "string",
"description": "e.g. Master"
},
"keywords": {
"type": "array",
"description": "List some keywords pertaining to this skill",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. HTML"
}
}
}
}
},
"languages": {
"type": "array",
"description": "List any other languages you speak",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"language": {
"type": "string",
"description": "e.g. English, Spanish"
},
"fluency": {
"type": "string",
"description": "e.g. Fluent, Beginner"
}
}
}
},
"interests": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Philosophy"
},
"keywords": {
"type": "array",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Friedrich Nietzsche"
}
}
}
}
},
"references": {
"type": "array",
"description": "List references you have received",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Timothy Cook"
},
"reference": {
"type": "string",
"description": "e.g. Joe blogs was a great employee, who turned up to work at least once a week. He exceeded my expectations when it came to doing nothing."
}
}
}
}
}
}

41
src/core/status-codes.js Normal file
View File

@ -0,0 +1,41 @@
/**
Status codes for HackMyResume.
@module core/status-codes
@license MIT. See LICENSE.MD for details.
*/
module.exports = {
success: 0,
themeNotFound: 1,
copyCss: 2,
resumeNotFound: 3,
missingCommand: 4,
invalidCommand: 5,
resumeNotFoundAlt: 6,
inputOutputParity: 7,
createNameMissing: 8,
pdfGeneration: 9,
missingPackageJSON: 10,
invalid: 11,
invalidFormat: 12,
notOnPath: 13,
readError: 14,
parseError: 15,
fileSaveError: 16,
generateError: 17,
invalidHelperUse: 18,
mixedMerge: 19,
invokeTemplate: 20,
compileTemplate: 21,
themeLoad: 22,
invalidParamCount: 23,
missingParam: 24,
createError: 25,
validateError: 26,
invalidOptionsFile: 27,
optionsFileNotFound: 28,
unknownSchema: 29,
themeHelperLoad: 30,
invalidSchemaVersion: 31
};

View File

@ -1,147 +0,0 @@
/**
Internal resume generation logic for FluentCMD.
@license Copyright (c) 2015 | James M. Devlin
*/
module.exports = function () {
// We don't mind pseudo-globals here
var path = require( 'path' )
, extend = require( './utils/extend' )
, unused = require('./utils/string')
, fs = require('fs')
, _ = require('underscore')
, FLUENT = require('fluentlib')
, PATH = require('path')
, MKDIRP = require('mkdirp')
, rez, _log, _err;
/**
Given a source JSON resume, a destination resume path, and a theme file,
generate 0..N resumes in the desired formats.
@param src Path to the source JSON resume file: "rez/resume.json".
@param dst An array of paths to the target resume file(s).
@param theme Friendly name of the resume theme. Defaults to "modern".
@param logger Optional logging override.
*/
function gen( src, dst, opts, logger, errHandler ) {
_log = logger || console.log;
_err = errHandler || error;
//_opts = extend( true, _opts, opts );
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim()) || 'modern';
_opts.prettify = opts.prettify === true ? _opts.prettify : false;
// Load input resumes...
if(!src || !src.length) { throw { fluenterror: 3 }; }
var sheets = src.map( function( res ) {
_log( 'Reading JSON resume: ' + res );
return (new FLUENT.Sheet()).open( res );
});
// Merge input resumes...
var msg = '';
rez = _.reduceRight( sheets, function( a, b, idx ) {
msg += ((idx == sheets.length - 2) ? 'Merging ' + a.meta.fileName : '')
+ ' onto ' + b.meta.fileName;
return extend( true, b, a );
});
msg && _log(msg);
// Load the active theme
// Verify the specified theme name/path
var tFolder = PATH.resolve( __dirname, '../node_modules/watermark/themes', _opts.theme );
var exists = require('./utils/file-exists');
if (!exists( tFolder )) {
tFolder = PATH.resolve( _opts.theme );
if (!exists( tFolder )) {
throw { fluenterror: 1, data: _opts.theme };
}
}
var theTheme = new FLUENT.Theme().open( tFolder );
_opts.themeObj = theTheme;
_log( 'Applying ' + theTheme.name.toUpperCase() + ' theme (' + Object.keys(theTheme.formats).length + ' formats)' );
// Expand output resumes... (can't use map() here)
var targets = [];
var that = this;
( (dst && dst.length && dst) || ['resume.all'] ).forEach( function(t) {
var to = path.resolve(t), pa = path.parse(to), fmat = pa.ext || '.all';
targets.push.apply(targets, fmat === '.all' ?
Object.keys( theTheme.formats ).map(function(k){ var z = theTheme.formats[k]; return { file: to.replace(/all$/g,z.pre), fmt: z } })
: [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]);
});
// Run the transformation!
var finished = targets.map( function(t) { return single(t, theTheme); } );
// Don't send the client back empty-handed
return { sheet: rez, targets: targets, processed: finished };
}
/**
Generate a single resume of a specific format.
@param f Full path to the destination resume to generate, for example,
"/foo/bar/resume.pdf" or "c:\foo\bar\resume.txt".
*/
function single( fi, theme ) {
try {
var f = fi.file, fType = fi.fmt.ext, fName = path.basename( f, '.' + fType );
var fObj = _.property( fi.fmt.pre )( theme.formats );
var fOut = path.join( f.substring( 0, f.lastIndexOf('.') + 1 ) + fObj.pre );
_log( 'Generating ' + fi.fmt.title.toUpperCase() + ' resume: ' + path.relative(process.cwd(), f ) );
var theFormat = _fmts.filter( function( fmt ) {
return fmt.name === fi.fmt.pre;
})[0];
MKDIRP( path.dirname(fOut) ); // Ensure dest folder exists; don't bug user
theFormat.gen.generate( rez, fOut, _opts );
}
catch( ex ) {
_err( ex );
}
}
/**
Handle an exception.
*/
function error( ex ) {
throw ex;
}
/**
Supported resume formats.
*/
var _fmts = [
{ name: 'html', ext: 'html', gen: new FLUENT.HtmlGenerator() },
{ name: 'txt', ext: 'txt', gen: new FLUENT.TextGenerator() },
{ name: 'doc', ext: 'doc', fmt: 'xml', gen: new FLUENT.WordGenerator() },
{ name: 'pdf', ext: 'pdf', fmt: 'html', is: false, gen: new FLUENT.HtmlPdfGenerator() },
{ name: 'md', ext: 'md', fmt: 'txt', gen: new FLUENT.MarkdownGenerator() },
{ name: 'json', ext: 'json', gen: new FLUENT.JsonGenerator() },
{ name: 'yml', ext: 'yml', fmt: 'yml', gen: new FLUENT.JsonYamlGenerator() }
];
/**
Default FluentCMD options.
*/
var _opts = {
theme: 'modern',
prettify: { // ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2,
unformatted: ['em','strong'],
max_char: 80, // ← See lib/html.js in above-linked repo
//wrap_line_length: 120, ← Don't use this
}
};
/**
Internal module interface. Used by FCV Desktop and HMR.
*/
return {
generate: gen,
options: _opts,
formats: _fmts
};
}();

View File

@ -0,0 +1,37 @@
/*
* decaffeinate suggestions:
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the BaseGenerator class.
@module generators/base-generator
@license MIT. See LICENSE.md for details.
*/
/**
The BaseGenerator class is the root of the generator hierarchy. Functionality
common to ALL generators lives here.
*/
let BaseGenerator;
module.exports = (BaseGenerator = (function() {
BaseGenerator = class BaseGenerator {
static initClass() {
/** Status codes. */
this.prototype.codes = require('../core/status-codes');
/** Generator options. */
this.prototype.opts = { };
}
/** Base-class initialize. */
constructor( format ) {
this.format = format;
}
};
BaseGenerator.initClass();
return BaseGenerator;
})());

View File

@ -0,0 +1,39 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the HTMLGenerator class.
@module generators/html-generator
@license MIT. See LICENSE.md for details.
*/
const TemplateGenerator = require('./template-generator');
const HTML = require('html');
require('string.prototype.endswith');
class HtmlGenerator extends TemplateGenerator {
constructor() { super('html'); }
/**
Copy satellite CSS files to the destination and optionally pretty-print
the HTML resume prior to saving.
*/
onBeforeSave( info ) {
if (info.outputFile.endsWith('.css')) {
return info.mk;
}
if (this.opts.prettify) {
return HTML.prettyPrint(info.mk, this.opts.prettify);
} else { return info.mk; }
}
}
module.exports = HtmlGenerator;

View File

@ -0,0 +1,129 @@
/*
* decaffeinate suggestions:
* DS103: Rewrite code to no longer use __guard__
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the HtmlPdfCLIGenerator class.
@module generators/html-pdf-generator.js
@license MIT. See LICENSE.md for details.
*/
const TemplateGenerator = require('./template-generator');
const FS = require('fs-extra');
const PATH = require('path');
const SLASH = require('slash');
const _ = require('underscore');
const HMSTATUS = require('../core/status-codes');
const SPAWN = require('../utils/safe-spawn');
/**
An HTML-driven PDF resume generator for HackMyResume. Talks to Phantom,
wkhtmltopdf, and other PDF engines over a CLI (command-line interface).
If an engine isn't installed for a particular platform, error out gracefully.
*/
class HtmlPdfCLIGenerator extends TemplateGenerator {
constructor() { super('pdf', 'html'); }
/** Generate the binary PDF. */
onBeforeSave( info ) {
//console.dir _.omit( info, 'mk' ), depth: null, colors: true
if ((info.ext !== 'html') && (info.ext !== 'pdf')) { return info.mk; }
let safe_eng = info.opts.pdf || 'wkhtmltopdf';
if (safe_eng === 'phantom') { safe_eng = 'phantomjs'; }
if (_.has(engines, safe_eng)) {
this.errHandler = info.opts.errHandler;
engines[ safe_eng ].call(this, info.mk, info.outputFile, info.opts, this.onError);
return null; // halt further processing
}
}
/* Low-level error callback for spawn(). May be called after HMR process
termination, so object references may not be valid here. That's okay; if
the references are invalid, the error was already logged. We could use
spawn-watch here but that causes issues on legacy Node.js. */
onError(ex, param) {
__guardMethod__(param.errHandler, 'err', o => o.err(HMSTATUS.pdfGeneration, ex));
}
}
module.exports = HtmlPdfCLIGenerator;
// TODO: Move each engine to a separate module
var engines = {
/**
Generate a PDF from HTML using wkhtmltopdf's CLI interface.
Spawns a child process with `wkhtmltopdf <source> <target>`. wkhtmltopdf
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease wkhtmltopdf rendering
*/
wkhtmltopdf(markup, fOut, opts, on_error) {
// Save the markup to a temporary file
const tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync(tempFile, markup, 'utf8');
// Prepare wkhtmltopdf arguments.
let wkopts = _.extend({'margin-top': '10mm', 'margin-bottom': '10mm'}, opts.wkhtmltopdf);
wkopts = _.flatten(_.map(wkopts, (v, k) => [`--${k}`, v]));
const wkargs = wkopts.concat([ tempFile, fOut ]);
SPAWN('wkhtmltopdf', wkargs , false, on_error, this);
},
/**
Generate a PDF from HTML using Phantom's CLI interface.
Spawns a child process with `phantomjs <script> <source> <target>`. Phantom
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease Phantom rendering
*/
phantomjs( markup, fOut, opts, on_error ) {
// Save the markup to a temporary file
const tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync(tempFile, markup, 'utf8');
let scriptPath = PATH.relative(process.cwd(), PATH.resolve( __dirname, '../utils/rasterize.js' ));
scriptPath = SLASH(scriptPath);
const sourcePath = SLASH(PATH.relative( process.cwd(), tempFile));
const destPath = SLASH(PATH.relative( process.cwd(), fOut));
SPAWN('phantomjs', [ scriptPath, sourcePath, destPath ], false, on_error, this);
},
/**
Generate a PDF from HTML using WeasyPrint's CLI interface.
Spawns a child process with `weasyprint <source> <target>`. Weasy Print
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
*/
weasyprint( markup, fOut, opts, on_error ) {
// Save the markup to a temporary file
const tempFile = fOut.replace(/\.pdf$/i, '.pdf.html');
FS.writeFileSync(tempFile, markup, 'utf8');
SPAWN('weasyprint', [tempFile, fOut], false, on_error, this);
}
};
function __guardMethod__(obj, methodName, transform) {
if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') {
return transform(obj, methodName);
} else {
return undefined;
}
}

View File

@ -0,0 +1,58 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the HtmlPngGenerator class.
@module generators/html-png-generator
@license MIT. See LICENSE.MD for details.
*/
const TemplateGenerator = require('./template-generator');
const FS = require('fs-extra');
const SLASH = require('slash');
const SPAWN = require('../utils/safe-spawn');
const PATH = require('path');
/**
An HTML-based PNG resume generator for HackMyResume.
*/
class HtmlPngGenerator extends TemplateGenerator {
constructor() { super('png', 'html'); }
invoke( /*rez, themeMarkup, cssInfo, opts*/ ) {}
// TODO: Not currently called or callable.
generate( rez, f, opts ) {
const htmlResults = opts.targets.filter(t => t.fmt.outFormat === 'html');
const htmlFile = htmlResults[0].final.files.filter(fl => fl.info.ext === 'html');
phantom(htmlFile[0].data, f);
}
}
module.exports = HtmlPngGenerator;
/**
Generate a PDF from HTML using Phantom's CLI interface.
Spawns a child process with `phantomjs <script> <source> <target>`. Phantom
must be installed and path-accessible.
TODO: If HTML generation has run, reuse that output
TODO: Local web server to ease Phantom rendering
*/
var phantom = function( markup, fOut ) {
// Save the markup to a temporary file
const tempFile = fOut.replace(/\.png$/i, '.png.html');
FS.writeFileSync(tempFile, markup, 'utf8');
const scriptPath = SLASH( PATH.relative( process.cwd(),
PATH.resolve( __dirname, '../utils/rasterize.js' ) ) );
const sourcePath = SLASH( PATH.relative( process.cwd(), tempFile) );
const destPath = SLASH( PATH.relative( process.cwd(), fOut) );
SPAWN('phantomjs', [ scriptPath, sourcePath, destPath ]);
};

View File

@ -0,0 +1,33 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the JsonGenerator class.
@module generators/json-generator
@license MIT. See LICENSE.md for details.
*/
const BaseGenerator = require('./base-generator');
const FS = require('fs');
const FJCV = require('fresh-jrs-converter');
/** The JsonGenerator generates a FRESH or JRS resume as an output. */
class JsonGenerator extends BaseGenerator {
constructor() { super('json'); }
invoke( rez ) {
let altRez = FJCV[ `to${rez.format() === 'FRESH' ? 'JRS' : 'FRESH'}` ](rez);
return altRez = FJCV.toSTRING( altRez );
}
//altRez.stringify()
generate( rez, f ) {
FS.writeFileSync(f, this.invoke(rez), 'utf8');
}
}
module.exports = JsonGenerator;

View File

@ -0,0 +1,41 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the JsonYamlGenerator class.
@module generators/json-yaml-generator
@license MIT. See LICENSE.md for details.
*/
const BaseGenerator = require('./base-generator');
const FS = require('fs');
const YAML = require('yamljs');
/**
JsonYamlGenerator takes a JSON resume object and translates it directly to
JSON without a template, producing an equivalent YAML-formatted resume. See
also YamlGenerator (yaml-generator.js).
*/
class JsonYamlGenerator extends BaseGenerator {
constructor() { super('yml'); }
invoke( rez/*, themeMarkup, cssInfo, opts*/ ) {
return YAML.stringify(JSON.parse( rez.stringify() ), Infinity, 2);
}
generate( rez, f/*, opts */) {
const data = YAML.stringify(JSON.parse( rez.stringify() ), Infinity, 2);
FS.writeFileSync(f, data, 'utf8');
return data;
}
}
module.exports = JsonYamlGenerator;

View File

@ -0,0 +1,17 @@
/**
Definition of the LaTeXGenerator class.
@module generators/latex-generator
@license MIT. See LICENSE.md for details.
*/
const TemplateGenerator = require('./template-generator');
/**
LaTeXGenerator generates a LaTeX resume via TemplateGenerator.
*/
class LaTeXGenerator extends TemplateGenerator {
constructor() { super('latex', 'tex'); }
}
module.exports = LaTeXGenerator;

View File

@ -0,0 +1,17 @@
/**
Definition of the MarkdownGenerator class.
@module generators/markdown-generator
@license MIT. See LICENSE.md for details.
*/
const TemplateGenerator = require('./template-generator');
/**
MarkdownGenerator generates a Markdown-formatted resume via TemplateGenerator.
*/
class MarkdownGenerator extends TemplateGenerator {
constructor() { super('md', 'txt'); }
}
module.exports = MarkdownGenerator;

View File

@ -0,0 +1,283 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the TemplateGenerator class. TODO: Refactor
@module generators/template-generator
@license MIT. See LICENSE.md for details.
*/
const FS = require('fs-extra');
const _ = require('underscore');
const MD = require('marked');
const XML = require('xml-escape');
const PATH = require('path');
const parsePath = require('parse-filepath');
const MKDIRP = require('mkdirp');
const BaseGenerator = require('./base-generator');
const EXTEND = require('extend');
/**
TemplateGenerator performs resume generation via local Handlebar or Underscore
style template expansion and is appropriate for text-based formats like HTML,
plain text, and XML versions of Microsoft Word, Excel, and OpenOffice.
@class TemplateGenerator
*/
class TemplateGenerator extends BaseGenerator {
/** Constructor. Set the output format and template format for this
generator. Will usually be called by a derived generator such as
HTMLGenerator or MarkdownGenerator. */
constructor( outputFormat, templateFormat/*, cssFile */) {
super(outputFormat);
this.tplFormat = templateFormat || outputFormat;
}
/** Generate a resume using string-based inputs and outputs without touching
the filesystem.
@method invoke
@param rez A FreshResume object.
@param opts Generator options.
@returns {Array} An array of objects representing the generated output
files. */
invoke( rez, opts ) {
opts =
opts
? (this.opts = EXTEND( true, { }, _defaultOpts, opts ))
: this.opts;
// Sort such that CSS files are processed before others
const curFmt = opts.themeObj.getFormat( this.format );
curFmt.files = _.sortBy(curFmt.files, fi => fi.ext !== 'css');
// Run the transformation!
const results = curFmt.files.map(function( tplInfo, idx ) {
let trx;
if (tplInfo.action === 'transform') {
trx = this.transform(rez, tplInfo.data, this.format, opts, opts.themeObj, curFmt);
if (tplInfo.ext === 'css') {
curFmt.files[idx].data = trx;
} else { tplInfo.ext === 'html'; }
}
//tplInfo.css contains the CSS data loaded by theme
//tplInfo.cssPath contains the absolute path to the source CSS File
//else {}
// Images and non-transformable binary files
if (typeof opts.onTransform === 'function') {
opts.onTransform(tplInfo);
}
return {info: tplInfo, data: trx};
}
, this);
return {files: results};
}
/** Generate a resume using file-based inputs and outputs. Requires access
to the local filesystem.
@method generate
@param rez A FreshResume object.
@param f Full path to the output resume file to generate.
@param opts Generator options. */
generate( rez, f, opts ) {
// Prepare
this.opts = EXTEND(true, { }, _defaultOpts, opts);
// Call the string-based generation method
const genInfo = this.invoke(rez, null);
const outFolder = parsePath( f ).dirname;
const curFmt = opts.themeObj.getFormat(this.format);
// Process individual files within this format. For example, the HTML
// output format for a theme may have multiple HTML files, CSS files,
// etc. Process them here.
genInfo.files.forEach(function( file ) {
// console.dir _.omit(file.info,'cssData','data','css' )
// Pre-processing
file.info.orgPath = file.info.orgPath || '';
const thisFilePath =
file.info.primary
? f
: PATH.join(outFolder, file.info.orgPath);
if ((file.info.action !== 'copy') && this.onBeforeSave) {
file.data = this.onBeforeSave({
theme: opts.themeObj,
outputFile: thisFilePath,
mk: file.data,
opts: this.opts,
ext: file.info.ext
});
if (!file.data) {
return;
}
}
// Write the file
if (typeof opts.beforeWrite === 'function') {
opts.beforeWrite({data: thisFilePath});
}
MKDIRP.sync(PATH.dirname( thisFilePath ));
if (file.info.action !== 'copy') {
FS.writeFileSync(thisFilePath, file.data, {encoding: 'utf8', flags: 'w'});
} else {
FS.copySync(file.info.path, thisFilePath);
}
if (typeof opts.afterWrite === 'function') {
opts.afterWrite({data: thisFilePath});
}
// Post-processing
if (this.onAfterSave) {
return this.onAfterSave({outputFile: thisFilePath, mk: file.data, opts: this.opts});
}
}
, this);
// Some themes require a symlink structure. If so, create it.
createSymLinks(curFmt, outFolder);
return genInfo;
}
/** Perform a single resume resume transformation using string-based inputs
and outputs without touching the local file system.
@param json A FRESH or JRS resume object.
@param jst The stringified template data
@param format The format name, such as "html" or "latex"
@param cssInfo Needs to be refactored.
@param opts Options and passthrough data. */
transform( json, jst, format, opts, theme, curFmt ) {
if (this.opts.freezeBreaks) {
jst = freeze(jst);
}
const eng = require(`../renderers/${theme.engine}-generator`);
let result = eng.generate(json, jst, format, curFmt, opts, theme);
if (this.opts.freezeBreaks) {
result = unfreeze(result);
}
return result;
}
}
module.exports = TemplateGenerator;
var createSymLinks = function( curFmt, outFolder ) {
// Some themes require a symlink structure. If so, create it.
if (curFmt.symLinks) {
Object.keys( curFmt.symLinks ).forEach(function(loc) {
const absLoc = PATH.join(outFolder, loc);
const absTarg = PATH.join(PATH.dirname(absLoc), curFmt.symLinks[loc]);
// Set type to 'file', 'dir', or 'junction' (Windows only)
const type = parsePath( absLoc ).extname ? 'file' : 'junction';
try {
return FS.symlinkSync(absTarg, absLoc, type);
} catch (err) {
let succeeded = false;
if (err.code === 'EEXIST') {
FS.unlinkSync(absLoc);
try {
FS.symlinkSync(absTarg, absLoc, type);
succeeded = true;
} catch (error) {
throw error;
}
}
if (!succeeded) {
throw err;
}
}
});
return;
}
};
/** Freeze newlines for protection against errant JST parsers. */
var freeze = function( markup ) {
markup.replace( _reg.regN, _defaultOpts.nSym );
return markup.replace( _reg.regR, _defaultOpts.rSym );
};
/** Unfreeze newlines when the coast is clear. */
var unfreeze = function( markup ) {
markup.replace(_reg.regSymR, '\r');
return markup.replace(_reg.regSymN, '\n');
};
/** Default template generator options. */
var _defaultOpts = {
engine: 'underscore',
keepBreaks: true,
freezeBreaks: false,
nSym: '&newl;', // newline entity
rSym: '&retn;', // return entity
template: {
interpolate: /\{\{(.+?)\}\}/g,
escape: /\{\{=(.+?)\}\}/g,
evaluate: /\{%(.+?)%\}/g,
comment: /\{#(.+?)#\}/g
},
filters: {
out( txt ) { return txt; },
raw( txt ) { return txt; },
xml( txt ) { return XML(txt); },
md( txt ) { return MD( txt || '' ); },
mdin( txt ) { return MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, ''); },
lower( txt ) { return txt.toLowerCase(); },
link( name, url ) {
if (url) { return `<a href="${url}">${name}</a>`; } else { return name; }
}
},
prettify: { // ← See https://github.com/beautify-web/js-beautify#options
indent_size: 2,
unformatted: ['em','strong','a'],
max_char: 80
} // ← See lib/html.js in above-linked repo
};
//wrap_line_length: 120, <-- Don't use this
/** Regexes for linebreak preservation. */
/* eslint-disable no-control-regex */
var _reg = {
regN: new RegExp( '\n', 'g' ),
regR: new RegExp( '\r', 'g' ),
regSymN: new RegExp( _defaultOpts.nSym, 'g' ),
regSymR: new RegExp( _defaultOpts.rSym, 'g' )
};
/* eslint-enable no-control-regex */

View File

@ -0,0 +1,16 @@
/**
Definition of the TextGenerator class.
@module generators/text-generator
@license MIT. See LICENSE.md for details.
*/
const TemplateGenerator = require('./template-generator');
/**
The TextGenerator generates a plain-text resume via the TemplateGenerator.
*/
class TextGenerator extends TemplateGenerator {
constructor() { super('txt'); }
}
module.exports = TextGenerator;

View File

@ -0,0 +1,14 @@
/*
Definition of the WordGenerator class.
@module generators/word-generator
@license MIT. See LICENSE.md for details.
*/
const TemplateGenerator = require('./template-generator');
class WordGenerator extends TemplateGenerator {
constructor() { super('doc', 'xml'); }
}
module.exports = WordGenerator;

View File

@ -0,0 +1,14 @@
/**
Definition of the XMLGenerator class.
@license MIT. See LICENSE.md for details.
@module generatprs/xml-generator
*/
const BaseGenerator = require('./base-generator');
/** The XmlGenerator generates an XML resume via the TemplateGenerator. */
class XMLGenerator extends BaseGenerator {
constructor() { super('xml'); }
}
module.exports = XMLGenerator;

View File

@ -0,0 +1,19 @@
/**
Definition of the YAMLGenerator class.
@module yaml-generator.js
@license MIT. See LICENSE.md for details.
*/
const TemplateGenerator = require('./template-generator');
/**
YamlGenerator generates a YAML-formatted resume via TemplateGenerator.
*/
class YAMLGenerator extends TemplateGenerator {
constructor() { super('yml', 'yml'); }
}
module.exports = YAMLGenerator;

View File

@ -0,0 +1,77 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Block helper definitions for HackMyResume / FluentCV.
@license MIT. See LICENSE.md for details.
@module helpers/generic-helpers
*/
const LO = require('lodash');
const _ = require('underscore');
require('../utils/string');
/** Block helper function definitions. */
module.exports = {
/**
Emit the enclosed content if the resume has a section with
the specified name. Otherwise, emit an empty string ''.
*/
section( title, options ) {
title = title.trim().toLowerCase();
const obj = LO.get(this.r, title);
let ret = '';
if (obj) {
if (_.isArray(obj)) {
if (obj.length) {
ret = options.fn(this);
}
} else if (_.isObject(obj)) {
if ((obj.history && obj.history.length) || (obj.sets && obj.sets.length)) {
ret = options.fn(this);
}
}
}
return ret;
},
ifHasSkill( rez, skill, options ) {
const skUp = skill.toUpperCase();
const ret = _.some(rez.skills.list, sk => (skUp.toUpperCase() === sk.name.toUpperCase()) && sk.years
, this);
if (ret) { return options.fn(this); }
},
/**
Emit the enclosed content if the resume has the named
property or subproperty.
*/
has( title, options ) {
title = title && title.trim().toLowerCase();
if (LO.get(this.r, title)) {
return options.fn(this);
}
},
/**
Return true if either value is truthy.
@method either
*/
either( lhs, rhs, options ) { if (lhs || rhs) { return options.fn(this); } }
};

View File

@ -0,0 +1,67 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Generic template helper definitions for command-line output.
@module console-helpers.js
@license MIT. See LICENSE.md for details.
*/
const PAD = require('string-padding');
const LO = require('lodash');
const CHALK = require('chalk');
const _ = require('underscore');
require('../utils/string');
module.exports = {
v( val, defaultVal, padding, style ) {
let retVal = ( (val === null) || (val === undefined) ) ? defaultVal : val;
let spaces = 0;
if (String.is(padding)) {
spaces = parseInt(padding, 10);
if (isNaN(spaces)) { spaces = 0; }
} else if (_.isNumber(padding)) {
spaces = padding;
}
if (spaces !== 0) {
retVal = PAD(retVal, Math.abs(spaces), null, spaces > 0 ? PAD.LEFT : PAD.RIGHT);
}
if (style && String.is( style )) {
retVal = LO.get( CHALK, style )( retVal );
}
return retVal;
},
gapLength(val) {
if (val < 35) {
return CHALK.green.bold(val);
} else if (val < 95) {
return CHALK.yellow.bold(val);
} else {
return CHALK.red.bold(val);
}
},
style( val, style ) {
return LO.get( CHALK, style )( val );
},
isPlural( val, options ) {
if (val > 1) {
return options.fn(this);
}
},
pad( val, spaces ) {
return PAD(val, Math.abs(spaces), null, spaces > 0 ? PAD.LEFT : PAD.RIGHT);
}
};

View File

@ -0,0 +1,703 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Generic template helper definitions for HackMyResume / FluentCV.
@license MIT. See LICENSE.md for details.
@module helpers/generic-helpers
*/
const MD = require('marked');
const H2W = require('../utils/html-to-wpml');
const XML = require('xml-escape');
const FluentDate = require('../core/fluent-date');
const HMSTATUS = require('../core/status-codes');
const moment = require('moment');
const FS = require('fs');
const LO = require('lodash');
const PATH = require('path');
const printf = require('printf');
const _ = require('underscore');
require('../utils/string');
/** Generic template helper function definitions. */
var GenericHelpers = (module.exports = {
/**
Emit a formatted string representing the specified datetime.
Convert the input date to the specified format through Moment.js. If date is
valid, return the formatted date string. If date is null, undefined, or other
falsy value, return the value of the 'fallback' parameter, if specified, or
null if no fallback was specified. If date is invalid, but not null/undefined/
falsy, return it as-is.
@param {string|Moment} datetime A date value.
@param {string} [dtFormat='YYYY-MM'] The desired datetime format. Must be a
Moment.js-compatible datetime format.
@param {string|Moment} fallback A fallback value to use if the specified date
is null, undefined, or falsy.
*/
formatDate(datetime, dtFormat, fallback) {
if (datetime == null) { datetime = undefined; }
if (dtFormat == null) { dtFormat = 'YYYY-MM'; }
// If a Moment.js object was passed in, just call format on it
if (datetime && moment.isMoment(datetime)) {
return datetime.format(dtFormat);
}
if (String.is(datetime)) {
// If a string was passed in, convert to Moment using the 2-paramter
// constructor with an explicit format string.
let momentDate = moment(datetime, dtFormat);
if (momentDate.isValid()) { return momentDate.format(dtFormat); }
// If that didn't work, try again with the single-parameter constructor
// but this may throw a deprecation warning
momentDate = moment(datetime);
if (momentDate.isValid()) { return momentDate.format(dtFormat); }
}
// We weren't able to format the provided datetime. Now do one of three
// things.
// 1. If datetime is non-null/non-falsy, return it. For this helper,
// string date values that we can't parse are assumed to be display dates.
// 2. If datetime IS null or falsy, use the value from the fallback.
// 3. If the fallback value is specifically 'true', emit 'Present'.
return datetime ||
(typeof fallback === 'string'
? fallback
: (fallback === true ? 'Present' : ''));
},
/**
Emit a formatted string representing the specified datetime.
@param {string} dateValue A raw date value from the FRESH or JRS resume.
@param {string} [dateFormat='YYYY-MM'] The desired datetime format. Must be
compatible with Moment.js datetime formats.
@param {string} [dateDefault=null] The default date value to use if the dateValue
parameter is null, undefined, or falsy.
*/
date(dateValue, dateFormat, dateDefault) {
if (!dateDefault || !String.is(dateDefault)) { dateDefault = 'Current'; }
if (!dateFormat || !String.is(dateFormat)) { dateFormat = 'YYYY-MM'; }
if (!dateValue || !String.is(dateValue)) { dateValue = null; }
if (!dateValue) { return dateDefault; }
const reserved = ['current', 'present', 'now'];
const dateValueSafe = dateValue.trim().toLowerCase();
if (_.contains(reserved, dateValueSafe)) { return dateValue; }
const dateValueMoment = moment(dateValue, dateFormat);
if (dateValueMoment.isValid()) { return dateValueMoment.format(dateFormat); }
return dateValue;
},
/**
Given a resume sub-object with a start/end date, format a representation of
the date range.
*/
dateRange( obj, fmt, sep, fallback ) {
if (!obj) { return ''; }
return _fromTo(obj.start, obj.end, fmt, sep, fallback);
},
/**
Format a from/to date range for display.
@method toFrom
*/
fromTo() { return _fromTo.apply(this, arguments); },
/**
Return a named color value as an RRGGBB string.
@method toFrom
*/
color( colorName, colorDefault ) {
// Key must be specified
if (!(colorName && colorName.trim())) {
return _reportError(HMSTATUS.invalidHelperUse,
{helper: 'fontList', error: HMSTATUS.missingParam, expected: 'name'});
} else {
if (!GenericHelpers.theme.colors) { return colorDefault; }
const ret = GenericHelpers.theme.colors[ colorName ];
if (!(ret && ret.trim())) {
return colorDefault;
}
return ret;
}
},
/**
Emit the size of the specified named font.
@param key {String} A named style from the "fonts" section of the theme's
theme.json file. For example: 'default' or 'heading1'.
*/
fontSize( key, defSize/*, units*/ ) {
let ret = '';
const hasDef = defSize && ( String.is( defSize ) || _.isNumber( defSize ));
// Key must be specified
if (!(key && key.trim())) {
_reportError( HMSTATUS.invalidHelperUse, {
helper: 'fontSize', error: HMSTATUS.missingParam, expected: 'key'
});
return ret;
} else if (GenericHelpers.theme.fonts) {
let fontSpec = LO.get( GenericHelpers.theme.fonts, this.format + '.' + key );
if (!fontSpec) {
// Check for an "all" format
if (GenericHelpers.theme.fonts.all) {
fontSpec = GenericHelpers.theme.fonts.all[ key ];
}
}
if( fontSpec ) {
// fontSpec can be a string, an array, or an object
if( String.is( fontSpec )) {
// No font size was specified, only a font family.
} else if( _.isArray( fontSpec )) {
// An array of fonts were specified. Each one could be a string
// or an object
if( !String.is( fontSpec[0] )) {
ret = fontSpec[0].size;
}
} else {
// A font description object.
ret = fontSpec.size;
}
}
}
// We weren't able to lookup the specified key. Default to defFont.
if (!ret) {
if (hasDef) {
ret = defSize;
} else {
_reportError( HMSTATUS.invalidHelperUse, {
helper: 'fontSize', error: HMSTATUS.missingParam,
expected: 'defSize'});
ret = '';
}
}
return ret;
},
/**
Emit the font face (such as 'Helvetica' or 'Calibri') associated with the
provided key.
@param key {String} A named style from the "fonts" section of the theme's
theme.json file. For example: 'default' or 'heading1'.
@param defFont {String} The font to use if the specified key isn't present.
Can be any valid font-face name such as 'Helvetica Neue' or 'Calibri'.
*/
fontFace( key, defFont ) {
let ret = '';
const hasDef = defFont && String.is( defFont );
// Key must be specified
if (!( key && key.trim())) {
_reportError( HMSTATUS.invalidHelperUse, {
helper: 'fontFace', error: HMSTATUS.missingParam, expected: 'key'
});
return ret;
// If the theme has a "fonts" section, lookup the font face.
} else if( GenericHelpers.theme.fonts ) {
let fontSpec = LO.get( GenericHelpers.theme.fonts, this.format + '.' + key);
if (!fontSpec) {
// Check for an "all" format
if (GenericHelpers.theme.fonts.all) {
fontSpec = GenericHelpers.theme.fonts.all[ key ];
}
}
if (fontSpec) {
// fontSpec can be a string, an array, or an object
if (String.is(fontSpec)) {
ret = fontSpec;
} else if (_.isArray(fontSpec)) {
// An array of fonts were specified. Each one could be a string
// or an object
ret = String.is( fontSpec[0] ) ? fontSpec[0] : fontSpec[0].name;
} else {
// A font description object.
ret = fontSpec.name;
}
}
}
// We weren't able to lookup the specified key. Default to defFont.
if (!(ret && ret.trim())) {
ret = defFont;
if (!hasDef) {
_reportError( HMSTATUS.invalidHelperUse, {
helper: 'fontFace', error: HMSTATUS.missingParam,
expected: 'defFont'});
ret = '';
}
}
return ret;
},
/**
Emit a comma-delimited list of font names suitable associated with the
provided key.
@param key {String} A named style from the "fonts" section of the theme's
theme.json file. For example: 'default' or 'heading1'.
@param defFontList {Array} The font list to use if the specified key isn't
present. Can be an array of valid font-face name such as 'Helvetica Neue'
or 'Calibri'.
@param sep {String} The default separator to use in the rendered output.
Defaults to ", " (comma with a space).
*/
fontList( key, defFontList, sep ) {
let ret = '';
const hasDef = defFontList && String.is( defFontList );
// Key must be specified
if (!( key && key.trim())) {
_reportError( HMSTATUS.invalidHelperUse, {
helper: 'fontList', error: HMSTATUS.missingParam, expected: 'key'
});
// If the theme has a "fonts" section, lookup the font list.
} else if (GenericHelpers.theme.fonts) {
let fontSpec = LO.get(GenericHelpers.theme.fonts, this.format + '.' + key);
if (!fontSpec) {
if (GenericHelpers.theme.fonts.all) {
fontSpec = GenericHelpers.theme.fonts.all[ key ];
}
}
if (fontSpec) {
// fontSpec can be a string, an array, or an object
if (String.is(fontSpec)) {
ret = fontSpec;
} else if (_.isArray(fontSpec)) {
// An array of fonts were specified. Each one could be a string
// or an object
fontSpec = fontSpec.map( ff => `'${String.is( ff ) ? ff : ff.name}'`);
ret = fontSpec.join( sep === undefined ? ', ' : (sep || '') );
} else {
// A font description object.
ret = fontSpec.name;
}
}
}
// The key wasn't found in the "fonts" section. Default to defFont.
if (!(ret && ret.trim())) {
if (!hasDef) {
_reportError( HMSTATUS.invalidHelperUse, {
helper: 'fontList', error: HMSTATUS.missingParam,
expected: 'defFontList'});
ret = '';
} else {
ret = defFontList;
}
}
return ret;
},
/**
Capitalize the first letter of the word. TODO: Rename
@method section
*/
camelCase(val) {
val = (val && val.trim()) || '';
if (val) { return (val.charAt(0).toUpperCase() + val.slice(1)); } else { return val; }
},
/**
Display a user-overridable section title for a FRESH resume theme. Use this in
lieue of hard-coding section titles.
Usage:
{{sectionTitle "sectionName"}}
{{sectionTitle "sectionName" "sectionTitle"}}
Example:
{{sectionTitle "Education"}}
{{sectionTitle "Employment" "Project History"}}
@param sect_name The name of the section being title. Must be one of the
top-level FRESH resume sections ("info", "education", "employment", etc.).
@param sect_title The theme-specified section title. May be replaced by the
user.
@method sectionTitle
*/
sectionTitle( sname, stitle ) {
// If not provided by the user, stitle should default to sname. ps.
// Handlebars silently passes in the options object to the last param,
// where in Underscore stitle will be null/undefined, so we check both.
// TODO: not actually sure that's true, given that we _.wrap these functions
stitle = (stitle && String.is(stitle) && stitle) || sname;
// If there's a section title override, use it.
return ( this.opts.stitles &&
this.opts.stitles[ sname.toLowerCase().trim() ] ) ||
stitle;
},
/** Convert inline Markdown to inline WordProcessingML. */
wpml( txt, inline ) {
if (!txt) { return ''; }
inline = (inline && !inline.hash) || false;
txt = XML(txt.trim());
txt = inline ? MD(txt).replace(/^\s*<p>|<\/p>\s*$/gi, '') : MD(txt);
txt = H2W( txt );
return txt;
},
/**
Emit a conditional link.
@method link
*/
link( text, url ) {
if (url && url.trim()) { return (`<a href="${url}">${text}</a>`); } else { return text; }
},
/**
Emit a conditional Markdown link.
@method link
*/
linkMD( text, url ) {
if (url && url.trim()) { return (`[${text}](${url})`); } else { return text; }
},
/**
Return the last word of the specified text.
@method lastWord
*/
lastWord( txt ) {
if (txt && txt.trim()) { return _.last( txt.split(' ') ); } else { return ''; }
},
/**
Convert a skill level to an RGB color triplet. TODO: refactor
@method skillColor
@param lvl Input skill level. Skill level can be expressed as a string
("beginner", "intermediate", etc.), as an integer (1,5,etc), as a string
integer ("1", "5", etc.), or as an RRGGBB color triplet ('#C00000',
'#FFFFAA').
*/
skillColor( lvl ) {
const idx = _skillLevelToIndex(lvl);
const skillColors = (this.theme && this.theme.palette &&
this.theme.palette.skillLevels) ||
[ '#FFFFFF', '#5CB85C', '#F1C40F', '#428BCA', '#C00000' ];
return skillColors[idx];
},
/**
Return an appropriate height. TODO: refactor
@method lastWord
*/
skillHeight( lvl ) {
const idx = _skillLevelToIndex(lvl);
return ['38.25', '30', '16', '8', '0'][idx];
},
/**
Return all but the last word of the input text.
@method initialWords
*/
initialWords( txt ) {
if (txt && txt.trim()) { return _.initial( txt.split(' ') ).join(' '); } else { return ''; }
},
/**
Trim the protocol (http or https) from a URL/
@method trimURL
*/
trimURL( url ) {
if (url && url.trim()) { return url.trim().replace(/^https?:\/\//i, ''); } else { return ''; }
},
/**
Convert text to lowercase.
@method toLower
*/
toLower( txt ) { if (txt && txt.trim()) { return txt.toLowerCase(); } else { return ''; } },
/**
Convert text to lowercase.
@method toLower
*/
toUpper( txt ) { if (txt && txt.trim()) { return txt.toUpperCase(); } else { return ''; } },
/**
Conditional stylesheet link. Creates a link to the specified stylesheet with
<link> or embeds the styles inline with <style></style>, depending on the
theme author's and user's preferences.
@param url {String} The path to the CSS file.
@param linkage {String} The default link method. Can be either `embed` or
`link`. If omitted, defaults to `embed`. Can be overridden by the `--css`
command-line switch.
*/
styleSheet( url, linkage ) {
// Establish the linkage style
linkage = this.opts.css || linkage || 'embed';
// Create the <link> or <style> tag
let ret = '';
if (linkage === 'link') {
ret = printf('<link href="%s" rel="stylesheet" type="text/css">', url);
} else {
const rawCss = FS.readFileSync(
PATH.join( this.opts.themeObj.folder, '/src/', url ), 'utf8' );
const renderedCss = this.engine.generateSimple( this, rawCss );
ret = printf('<style>%s</style>', renderedCss );
}
// If the currently-executing template is inherited, append styles
if (this.opts.themeObj.inherits && this.opts.themeObj.inherits.html && (this.format === 'html')) {
ret +=
(linkage === 'link')
? `<link href="${this.opts.themeObj.overrides.path}" rel="stylesheet" type="text/css">`
: `<style>${this.opts.themeObj.overrides.data}</style>`;
}
// TODO: It would be nice to use Handlebar.SafeString here, but these
// are supposed to be generic helpers. Provide an equivalent, or expose
// it when Handlebars is the chosen engine, which is most of the time.
return ret;
},
/**
Perform a generic comparison.
See: http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates
@method compare
*/
compare(lvalue, rvalue, options) {
if (arguments.length < 3) {
throw new Error('Template helper \'compare\' needs 2 parameters');
}
const operator = options.hash.operator || '==';
const operators = {
'=='(l,r) { return l === r; },
'==='(l,r) { return l === r; },
'!='(l,r) { return l !== r; },
'<'(l,r) { return l < r; },
'>'(l,r) { return l > r; },
'<='(l,r) { return l <= r; },
'>='(l,r) { return l >= r; },
'typeof'(l,r) { return typeof l === r; }
};
if (!operators[operator]) {
throw new Error(`Helper 'compare' doesn't know the operator ${operator}`);
}
const result = operators[operator]( lvalue, rvalue );
if (result) { return options.fn(this); } else { return options.inverse(this); }
},
/**
Emit padded text.
*/
pad(stringOrArray, padAmount/*, unused*/ ) {
stringOrArray = stringOrArray || '';
padAmount = padAmount || 0;
let ret = '';
const PAD = require('string-padding');
if (!String.is(stringOrArray)) {
ret = stringOrArray
.map(line => PAD(line, line.length + Math.abs(padAmount), null, padAmount < 0 ? PAD.LEFT : PAD.RIGHT))
.join('\n');
} else {
ret = PAD(stringOrArray, stringOrArray.length + Math.abs(padAmount), null, padAmount < 0 ? PAD.LEFT : PAD.RIGHT);
}
return ret;
},
/**
Given the name of a skill ("JavaScript" or "HVAC repair"), return the number
of years assigned to that skill in the resume.skills.list collection.
*/
skillYears( skill, rez ) {
const sk = _.find(rez.skills.list, sk => sk.name.toUpperCase() === skill.toUpperCase());
if (sk) { return sk.years; } else { return '?'; }
},
/**
Given an object that may be a string or an object, return it as-is if it's a
string, otherwise return the value at obj[objPath].
*/
stringOrObject( obj, objPath/*, rez */) {
if (_.isString(obj)) { return obj; } else { return LO.get(obj, objPath); }
}
});
/**
Report an error to the outside world without throwing an exception. Currently
relies on kludging the running verb into. opts.
*/
var _reportError = ( code, params ) => GenericHelpers.opts.errHandler.err( code, params );
/**
Format a from/to date range for display.
*/
var _fromTo = function( dateA, dateB, fmt, sep, fallback ) {
// Prevent accidental use of safe.start, safe.end, safe.date
// The dateRange helper is for raw dates only
if (moment.isMoment( dateA ) || moment.isMoment( dateB )) {
_reportError( HMSTATUS.invalidHelperUse, { helper: 'dateRange' } );
return '';
}
let dateFrom = null;
let dateTo = null;
let dateTemp = null;
// Check for 'current', 'present', 'now', '', null, and undefined
dateA = dateA || '';
dateB = dateB || '';
const dateATrim = dateA.trim().toLowerCase();
const dateBTrim = dateB.trim().toLowerCase();
const reserved = ['current','present','now', ''];
fmt = (fmt && String.is(fmt) && fmt) || 'YYYY-MM';
sep = (sep && String.is(sep) && sep) || ' — ';
if (_.contains( reserved, dateATrim )) {
dateFrom = fallback || '???';
} else {
dateTemp = FluentDate.fmt( dateA );
dateFrom = dateTemp.format( fmt );
}
if (_.contains( reserved, dateBTrim )) {
dateTo = fallback || 'Present';
} else {
dateTemp = FluentDate.fmt( dateB );
dateTo = dateTemp.format( fmt );
}
if (dateFrom === dateTo) {
return dateFrom;
} else if (dateFrom && dateTo) {
return dateFrom + sep + dateTo;
} else if (dateFrom || dateTo) {
return dateFrom || dateTo;
}
return '';
};
var _skillLevelToIndex = function( lvl ) {
let idx = 0;
if (String.is( lvl )) {
lvl = lvl.trim().toLowerCase();
const intVal = parseInt( lvl );
if (isNaN(intVal)) {
switch (lvl) {
case 'beginner': idx = 1; break;
case 'intermediate': idx = 2; break;
case 'advanced': idx = 3; break;
case 'master': idx = 4; break;
}
} else {
idx = Math.min( intVal / 2, 4 );
idx = Math.max( 0, idx );
}
} else {
idx = Math.min( lvl / 2, 4 );
idx = Math.max( 0, idx );
}
return idx;
};
// Note [1] --------------------------------------------------------------------
// Make sure it's precisely a string or array since some template engines jam
// their options/context object into the last parameter and we are allowing the
// defFont parameter to be omitted in certain cases. This is a little kludgy,
// but works fine for this case. If we start doing this regularly, we should
// rebind these parameters.
// Note [2]: -------------------------------------------------------------------
// If execution reaches here, some sort of cosmic ray or sunspot has landed on
// HackMyResume, or a theme author is deliberately messing with us by doing
// something like:
//
// "fonts": {
// "default": "",
// "heading1": null
// }
//
// Rather than sort it out, we'll just fall back to defFont.

View File

@ -0,0 +1,89 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Template helper definitions for Handlebars.
@license MIT. See LICENSE.md for details.
@module handlebars-helpers.js
*/
const HANDLEBARS = require('handlebars');
const _ = require('underscore');
const helpers = require('./generic-helpers');
const path = require('path');
const blockHelpers = require('./block-helpers');
const HMS = require('../core/status-codes');
/**
Register useful Handlebars helpers.
@method registerHelpers
*/
module.exports = function( theme, rez, opts ) {
helpers.theme = theme;
helpers.opts = opts;
helpers.type = 'handlebars';
// Prepare generic helpers for use with Handlebars. We do this by wrapping them
// in a Handlebars-aware wrapper which calls the helper internally.
const wrappedHelpers = _.mapObject(helpers, function( hVal/*, hKey*/ ) {
if (_.isFunction(hVal)) {
return _.wrap(hVal, function(func) {
const args = Array.prototype.slice.call(arguments);
args.shift(); // lose the 1st element (func) [^1]
//args.pop() # lose the last element (HB options hash)
args[ args.length - 1 ] = rez; // replace w/ resume object
return func.apply(this, args);
}); // call the generic helper
}
return hVal;
}
, this);
HANDLEBARS.registerHelper(wrappedHelpers);
// Prepare Handlebars-specific helpers - "blockHelpers" is really a misnomer
// since any kind of Handlebars-specific helper can live here
HANDLEBARS.registerHelper(blockHelpers);
// Register any theme-provided custom helpers...
// Normalize "theme.helpers" (string or array) to an array
if (_.isString(theme.helpers)) { theme.helpers = [ theme.helpers ]; }
if (_.isArray(theme.helpers)) {
const glob = require('glob');
const slash = require('slash');
let curGlob = null;
try {
_.each(theme.helpers, function(fGlob) { // foreach theme.helpers entry
curGlob = fGlob; // ..cache in case of exception
fGlob = path.join(theme.folder, fGlob); // ..make relative to theme
const files = glob.sync(slash(fGlob)); // ..expand the glob
if (files.length > 0) { // ..guard against empty glob
_.each(files, function(f) { // ..loop over concrete paths
HANDLEBARS.registerHelper(require(f)); // ..register the path
});
} else {
throw {fluenterror: HMS.themeHelperLoad, inner: null, glob: fGlob};
}
});
return;
} catch (ex) {
throw{
fluenterror: HMS.themeHelperLoad,
inner: ex,
glob: curGlob, exit: true
};
}
}
};
// [^1]: This little bit of acrobatics ensures that our generic helpers are
// called as generic helpers, not as Handlebars-specific helpers. This allows
// them to be used in other templating engines, like Underscore. If you need a
// Handlebars-specific helper with normal Handlebars context and options, put it
// in block-helpers.coffee.

View File

@ -0,0 +1,35 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Template helper definitions for Underscore.
@license MIT. See LICENSE.md for details.
@module handlebars-helpers.js
*/
const _ = require('underscore');
const helpers = require('./generic-helpers');
/**
Register useful Underscore helpers.
@method registerHelpers
*/
module.exports = function( theme, opts, cssInfo, ctx, eng ) {
helpers.theme = theme;
helpers.opts = opts;
helpers.cssInfo = cssInfo;
helpers.engine = eng;
ctx.h = helpers;
_.each(helpers, function( hVal ) {
if (_.isFunction(hVal)) {
return _.bind(hVal, ctx);
}
}
, this);
};

View File

@ -1,68 +1,46 @@
#! /usr/bin/env node
/**
Command-line interface (CLI) for FluentCMD via Node.js.
@license Copyright (c) 2015 | James M. Devlin
External API surface for HackMyResume.
@license MIT. See LICENSE.md for details.
@module hackmycore/index
*/
var ARGS = require( 'minimist' )
, FCMD = require( './fluentcmd')
, PKG = require('../package.json');
try {
var opts = { };
main();
}
catch( ex ) {
handleError( ex );
}
function main() {
/** API facade for HackMyResume. */
// Setup.
if( process.argv.length <= 2 ) { throw { fluenterror: 3 }; }
var args = ARGS( process.argv.slice(2) );
opts = getOpts( args );
logMsg( '*** FluentCMD v' + PKG.version + ' ***' );
module.exports = {
// Convert arguments to source files, target files, options
var src = args._ || [];
var dst = (args.o && ((typeof args.o === 'string' && [ args.o ]) || args.o)) || [];
dst = (dst === true) ? [] : dst; // Handle -o with missing output file
verbs: {
build: require('./verbs/build'),
analyze: require('./verbs/analyze'),
validate: require('./verbs/validate'),
convert: require('./verbs/convert'),
new: require('./verbs/create'),
peek: require('./verbs/peek')
},
// Generate!
FCMD.generate( src, dst, opts, logMsg );
process.platform !== 'win32' && console.log('\n');
}
alias: {
generate: require('./verbs/build'),
create: require('./verbs/create')
},
function logMsg( msg ) {
opts.silent || console.log( msg );
}
function getOpts( args ) {
var noPretty = args['nopretty'] || args.n;
noPretty = noPretty && (noPretty === true || noPretty === 'true');
return {
theme: args.t || 'modern',
prettify: !noPretty,
silent: args.s || args.silent
};
}
function handleError( ex ) {
var msg = '';
if( ex.fluenterror ){
switch( ex.fluenterror ) { // TODO: Remove magic numbers
case 1: msg = "The specified theme couldn't be found: " + ex.data; break;
case 2: msg = "Couldn't copy CSS file to destination folder"; break;
case 3: msg = "Please specify a valid JSON resume file."; break;
};
}
else {
msg = ex.toString();
}
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
console.log( 'ERROR: ' + trimmed.toString() );
}
options: require('./core/default-options'),
formats: require('./core/default-formats'),
Sheet: require('./core/fresh-resume'),
FRESHResume: require('./core/fresh-resume'),
JRSResume: require('./core/jrs-resume'),
FRESHTheme: require('./core/fresh-theme'),
JRSTheme: require('./core/jrs-theme'),
ResumeFactory: require('./core/resume-factory'),
FluentDate: require('./core/fluent-date'),
HtmlGenerator: require('./generators/html-generator'),
TextGenerator: require('./generators/text-generator'),
HtmlPdfCliGenerator: require('./generators/html-pdf-cli-generator'),
WordGenerator: require('./generators/word-generator'),
MarkdownGenerator: require('./generators/markdown-generator'),
JsonGenerator: require('./generators/json-generator'),
YamlGenerator: require('./generators/yaml-generator'),
JsonYamlGenerator: require('./generators/json-yaml-generator'),
LaTeXGenerator: require('./generators/latex-generator'),
HtmlPngGenerator: require('./generators/html-png-generator')
};

View File

@ -0,0 +1,54 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const FluentDate = require('../core/fluent-date');
const _ = require('underscore');
const lo = require('lodash');
module.exports = {
/**
Compute the total duration of the work history.
@returns The total duration of the sheet's work history, that is, the number
of years between the start date of the earliest job on the resume and the
*latest end date of all jobs in the work history*. This last condition is for
sheets that have overlapping jobs.
*/
run(rez, collKey, startKey, endKey, unit) {
unit = unit || 'years';
const hist = lo.get(rez, collKey);
if (!hist || !hist.length) { return 0; }
// BEGIN CODE DUPLICATION --> src/inspectors/gap-inspector.coffee (TODO)
// Convert the candidate's employment history to an array of dates,
// where each element in the array is a start date or an end date of a
// job -- it doesn't matter which.
let new_e = hist.map(function( job ) {
let obj = _.pick( job, [startKey, endKey] );
// Synthesize an end date if this is a "current" gig
if (!_.has(obj, endKey)) { obj[endKey] = 'current'; }
if (obj && (obj[startKey] || obj[endKey])) {
obj = _.pairs(obj);
obj[0][1] = FluentDate.fmt( obj[0][1] );
if (obj.length > 1) {
obj[1][1] = FluentDate.fmt( obj[1][1] );
}
}
return obj;
});
// Flatten the array, remove empties, and sort
new_e = _.filter(_.flatten( new_e, true ), v => v && v.length && v[0] && v[0].length);
if (!new_e || !new_e.length) { return 0; }
new_e = _.sortBy(new_e, elem => elem[1].unix());
// END CODE DUPLICATION
const firstDate = _.first( new_e )[1];
const lastDate = _.last( new_e )[1];
return lastDate.diff(firstDate, unit);
}
};

View File

@ -0,0 +1,157 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Employment gap analysis for HackMyResume.
@license MIT. See LICENSE.md for details.
@module inspectors/gap-inspector
*/
const _ = require('underscore');
const FluentDate = require('../core/fluent-date');
const moment = require('moment');
const LO = require('lodash');
/**
Identify gaps in the candidate's employment history.
*/
module.exports = {
moniker: 'gap-inspector',
/**
Run the Gap Analyzer on a resume.
@method run
@return {Array} An array of object representing gaps in the candidate's
employment history. Each object provides the start, end, and duration of the
gap:
{ <-- gap
start: // A Moment.js date
end: // A Moment.js date
duration: // Gap length
}
*/
run(rez) {
// This is what we'll return
const coverage = {
gaps: [],
overlaps: [],
pct: '0%',
duration: {
total: 0,
work: 0,
gaps: 0
}
};
// Missing employment section? Bye bye.
const hist = LO.get(rez, 'employment.history');
if (!hist || !hist.length) { return coverage; }
// Convert the candidate's employment history to an array of dates,
// where each element in the array is a start date or an end date of a
// job -- it doesn't matter which.
let new_e = hist.map( function( job ) {
let obj = _.pick( job, ['start', 'end'] );
if (obj && (obj.start || obj.end)) {
obj = _.pairs( obj );
obj[0][1] = FluentDate.fmt( obj[0][1] );
if (obj.length > 1) {
obj[1][1] = FluentDate.fmt( obj[1][1] );
}
}
return obj;
});
// Flatten the array, remove empties, and sort
new_e = _.filter(_.flatten( new_e, true ), v => v && v.length && v[0] && v[0].length);
if (!new_e || !new_e.length) { return coverage; }
new_e = _.sortBy(new_e, elem => elem[1].unix());
// Iterate over elements in the array. Each time a start date is found,
// increment a reference count. Each time an end date is found, decrement
// the reference count. When the reference count reaches 0, we have a gap.
// When the reference count is > 0, the candidate is employed. When the
// reference count reaches 2, the candidate is overlapped.
let ref_count = 0;
let total_gap_days = 0;
new_e.forEach(function(point) {
const inc = point[0] === 'start' ? 1 : -1;
ref_count += inc;
// If the ref count just reached 0, start a new GAP
if (ref_count === 0) {
return coverage.gaps.push( { start: point[1], end: null });
// If the ref count reached 1 by rising, end the last GAP
} else if ((ref_count === 1) && (inc === 1)) {
const lastGap = _.last( coverage.gaps );
if (lastGap) {
lastGap.end = point[1];
lastGap.duration = lastGap.end.diff( lastGap.start, 'days' );
return total_gap_days += lastGap.duration;
}
// If the ref count reaches 2 by rising, start a new OVERLAP
} else if ((ref_count === 2) && (inc === 1)) {
return coverage.overlaps.push( { start: point[1], end: null });
// If the ref count reaches 1 by falling, end the last OVERLAP
} else if ((ref_count === 1) && (inc === -1)) {
const lastOver = _.last( coverage.overlaps );
if (lastOver) {
lastOver.end = point[1];
lastOver.duration = lastOver.end.diff( lastOver.start, 'days' );
if (lastOver.duration === 0) {
return coverage.overlaps.pop();
}
}
}
});
// It's possible that the last gap/overlap didn't have an explicit .end
// date.If so, set the end date to the present date and compute the
// duration normally.
if (coverage.overlaps.length) {
const o = _.last( coverage.overlaps );
if (o && !o.end) {
o.end = moment();
o.duration = o.end.diff( o.start, 'days' );
}
}
if (coverage.gaps.length) {
const g = _.last( coverage.gaps );
if (g && !g.end) {
g.end = moment();
g.duration = g.end.diff( g.start, 'days' );
}
}
// Package data for return to the client
const tdur = rez.duration('days');
const dur = {
total: tdur,
work: tdur - total_gap_days,
gaps: total_gap_days
};
coverage.pct = (dur.total > 0) && (dur.work > 0) ? ((((dur.total - dur.gaps) / dur.total) * 100)).toFixed(1) + '%' : '???';
coverage.duration = dur;
return coverage;
}
};

View File

@ -0,0 +1,74 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Keyword analysis for HackMyResume.
@license MIT. See LICENSE.md for details.
@module inspectors/keyword-inspector
*/
/**
Analyze the resume's use of keywords.
TODO: BUG: Keyword search regex is inaccurate, especially for one or two
letter keywords like "C" or "CLI".
@class keywordInspector
*/
module.exports = {
/** A unique name for this inspector. */
moniker: 'keyword-inspector',
/**
Run the Keyword Inspector on a resume.
@method run
@return An collection of statistical keyword data.
*/
run( rez ) {
// "Quote" or safely escape a keyword so it can be used as a regex. For
// example, if the keyword is "C++", yield "C\+\+".
// http://stackoverflow.com/a/2593661/4942583
const regex_quote = str => (str + '').replace(/[.?*+^$[\]\\(){}|-]/ig, '\\$&');
// Create a searchable plain-text digest of the resume
// TODO: BUG: Don't search within keywords for other keywords. Job A
// declares the "foo" keyword. Job B declares the "foo & bar" keyword. Job
// B's mention of "foobar" should not count as a mention of "foo".
// To achieve this, remove keywords from the search digest and treat them
// separately.
let searchable = '';
rez.transformStrings(['imp', 'computed', 'safe'], ( key, val ) => searchable += ` ${val}`);
// Assemble a regex skeleton we can use to test for keywords with a bit
// more
const prefix = `(?:${['^', '\\s+', '[\\.,]+'].join('|')})`;
const suffix = `(?:${['$', '\\s+', '[\\.,]+'].join('|')})`;
return rez.keywords().map(function(kw) {
// 1. Using word boundary or other regex class is inaccurate
//
// var regex = new RegExp( '\\b' + regex_quote( kw )/* + '\\b'*/, 'ig');
//
// 2. Searching for the raw keyword is inaccurate ("C" will match any
// word containing a 'c'!).
//
// var regex = new RegExp( regex_quote( kw ), 'ig');
//
// 3. Instead, use a custom regex with special delimeters.
const regex_str = prefix + regex_quote( kw ) + suffix;
const regex = new RegExp( regex_str, 'ig');
let count = 0;
while (regex.exec( searchable ) !== null) {
count++;
}
return {
name: kw,
count
};
});
}
};

View File

@ -0,0 +1,46 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Section analysis for HackMyResume.
@license MIT. See LICENSE.md for details.
@module inspectors/totals-inspector
*/
const _ = require('underscore');
/**
Retrieve sectional overview and summary information.
@class totalsInspector
*/
module.exports = {
moniker: 'totals-inspector',
/**
Run the Totals Inspector on a resume.
@method run
@return An object containing summary information for each section on the
resume.
*/
run( rez ) {
const sectionTotals = { };
_.each(rez, function(val, key) {
if (_.isArray( val ) && !_.isString(val)) {
return sectionTotals[ key ] = val.length;
} else if (val.history && _.isArray( val.history )) {
return sectionTotals[ key ] = val.history.length;
} else if (val.sets && _.isArray( val.sets )) {
return sectionTotals[ key ] = val.sets.length;
}
});
return {
totals: sectionTotals,
numSections: Object.keys( sectionTotals ).length
};
}
};

View File

@ -0,0 +1,118 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the HandlebarsGenerator class.
@license MIT. See LICENSE.md for details.
@module renderers/handlebars-generator
*/
const _ = require('underscore');
const HANDLEBARS = require('handlebars');
const FS = require('fs');
const registerHelpers = require('../helpers/handlebars-helpers');
const PATH = require('path');
const parsePath = require('parse-filepath');
const READFILES = require('recursive-readdir-sync');
const HMSTATUS = require('../core/status-codes');
const SLASH = require('slash');
/**
Perform template-based resume generation using Handlebars.js.
@class HandlebarsGenerator
*/
module.exports = {
generateSimple( data, tpl ) {
let template;
try {
// Compile and run the Handlebars template.
template = HANDLEBARS.compile(tpl, {
strict: false,
assumeObjects: false,
noEscape: data.opts.noescape
}
);
return template(data);
} catch (err) {
throw{
fluenterror:
HMSTATUS[ template ? 'invokeTemplate' : 'compileTemplate' ],
inner: err
};
}
},
generate( json, jst, format, curFmt, opts, theme ) {
// Preprocess text
let encData = json;
if ((format === 'html') || (format === 'pdf')) {
encData = json.markdownify();
}
if( format === 'doc' ) {
encData = json.xmlify();
}
// Set up partials and helpers
registerPartials(format, theme);
registerHelpers(theme, encData, opts);
// Set up the context
const ctx = {
r: encData,
RAW: json,
filt: opts.filters,
format,
opts,
engine: this,
results: curFmt.files,
headFragment: opts.headFragment || ''
};
// Render the template
return this.generateSimple(ctx, jst);
}
};
var registerPartials = function(format, theme) {
if (_.contains( ['html','doc','md','txt','pdf'], format )) {
// Locate the global partials folder
const partialsFolder = PATH.join(
parsePath( require.resolve('fresh-themes') ).dirname,
'/partials/',
format === 'pdf' ? 'html' : format
);
// Register global partials in the /partials/[format] folder
// TODO: Only do this once per HMR invocation.
_.each(READFILES( partialsFolder ), function( el ) {
const name = SLASH(PATH.relative( partialsFolder, el ).replace(/\.(?:html|xml|hbs|md|txt)$/i, ''));
const tplData = FS.readFileSync(el, 'utf8');
const compiledTemplate = HANDLEBARS.compile(tplData);
HANDLEBARS.registerPartial(name, compiledTemplate);
return theme.partialsInitialized = true;
});
}
// Register theme-specific partials
return _.each(theme.partials, function( el ) {
const tplData = FS.readFileSync(el.path, 'utf8');
const compiledTemplate = HANDLEBARS.compile(tplData);
return HANDLEBARS.registerPartial(el.name, compiledTemplate);
});
};

View File

@ -0,0 +1,45 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the JRSGenerator class.
@license MIT. See LICENSE.md for details.
@module renderers/jrs-generator
*/
const MD = require('marked');
/**
Perform template-based resume generation for JSON Resume themes.
@class JRSGenerator
*/
module.exports = {
generate( json, jst, format, cssInfo, opts, theme ) {
// Disable JRS theme chatter (console.log, console.error, etc.)
const turnoff = ['log', 'error', 'dir'];
const org = turnoff.map(function(c) {
const ret = console[c]; // eslint-disable-line no-console
console[c] = function() {}; // eslint-disable-line no-console
return ret;
});
// Freeze and render
let rezHtml = theme.render(json.harden());
// Turn logging back on
turnoff.forEach((c, idx) => console[c] = org[idx]); // eslint-disable-line no-console
// Unfreeze and apply Markdown
return rezHtml = rezHtml.replace(/@@@@~[\s\S]*?~@@@@/g, val => MDIN( val.replace( /~@@@@/g,'' ).replace( /@@@@~/g,'' ) ));
}
};
var MDIN = txt => // TODO: Move this
MD(txt || '' ).replace(/^\s*<p>|<\/p>\s*$/gi, '')
;

View File

@ -0,0 +1,90 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the UnderscoreGenerator class.
@license MIT. See LICENSE.md for details.
@module underscore-generator.js
*/
const _ = require('underscore');
const registerHelpers = require('../helpers/underscore-helpers');
require('../utils/string');
const escapeLaTeX = require('escape-latex');
/**
Perform template-based resume generation using Underscore.js.
@class UnderscoreGenerator
*/
module.exports = {
generateSimple( data, tpl ) {
let t;
try {
// Compile and run the Handlebars template.
t = _.template(tpl);
return t(data);
} catch (err) {
//console.dir _error
const HMS = require('../core/status-codes');
throw{
fluenterror: HMS[t ? 'invokeTemplate' : 'compileTemplate'],
inner: err
};
}
},
generate( json, jst, format, cssInfo, opts, theme ) {
// Tweak underscore's default template delimeters
let delims = (opts.themeObj && opts.themeObj.delimeters) || opts.template;
if (opts.themeObj && opts.themeObj.delimeters) {
delims = _.mapObject(delims, (val) => new RegExp(val, 'ig'));
}
_.templateSettings = delims;
// Massage resume strings / text
let r = null;
switch (format) {
case 'html': r = json.markdownify(); break;
case 'pdf': r = json.markdownify(); break;
case 'png': r = json.markdownify(); break;
case 'latex':
var traverse = require('traverse');
r = traverse(json).map(function() {
if (this.isLeaf && String.is(this.node)) {
return escapeLaTeX(this.node);
}
return this.node;
});
break;
default: r = json;
}
// Set up the context
const ctx = {
r,
filt: opts.filters,
XML: require('xml-escape'),
RAW: json,
cssInfo,
//engine: @
headFragment: opts.headFragment || '',
opts
};
// Link to our helpers
registerHelpers(theme, opts, cssInfo, ctx, this);
// Generate!
return this.generateSimple(ctx, jst);
}
};

View File

@ -1,78 +0,0 @@
/**
Plain JavaScript replacement of jQuery .extend based on jQuery sources.
@license Copyright (c) 2015 | James M. Devlin
*/
function _extend() {
function isPlainObject( obj ) {
if ((typeof obj !== "object") || obj.nodeType ||
(obj !== null && obj === obj.window)) {
return false;
}
if (obj.constructor &&
!hasOwnProperty.call( obj.constructor.prototype, "isPrototypeOf" )) {
return false;
}
return true;
}
var options
, name
, src
, copy
, copyIsArray
, clone
, target = arguments[0] || {}
, i = 1
, length = arguments.length
, deep = false;
// Handle a deep copy situation
if (typeof target === "boolean") {
deep = target;
// Skip the boolean and the target
target = arguments[i] || {};
i++;
}
// Handle case when target is a string or something (possible in deep copy)
//if (typeof target !== "object" && !jQuery.isFunction(target))
if (typeof target !== "object" && typeof target !== "function")
target = {};
for (; i < length; i++) {
// Only deal with non-null/undefined values
if ((options = arguments[i]) !== null) {
// Extend the base object
for (name in options) {
src = target[name];
copy = options[name];
// Prevent never-ending loop
if (target === copy) continue;
// Recurse if we're merging plain objects or arrays
if (deep && copy && (isPlainObject(copy) ||
(copyIsArray = (copy.constructor === Array)))) {
if (copyIsArray) {
copyIsArray = false;
clone = src && (src.constructor === Array) ? src : [];
} else {
clone = src && isPlainObject(src) ? src : {};
}
// Never move original objects, clone them
target[name] = _extend(deep, clone, copy);
// Don't bring in undefined values
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
// Return the modified object
return target;
}
module.exports = _extend;

View File

@ -0,0 +1,11 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the SyntaxErrorEx class.
@module file-contains.js
*/
module.exports = ( file, needle ) => require('fs').readFileSync(file,'utf-8').indexOf( needle ) > -1;

View File

@ -1,18 +0,0 @@
/**
File-exists checker for Node.js.
@license Copyright (c) 2015 | James M. Devlin
*/
var FS = require('fs');
// Yup, this is now the recommended way to check for file existence on Node.
// fs.exists is deprecated and the recommended fs.statSync/lstatSync throws
// exceptions on non-existent paths :)
module.exports = function (path) {
try {
FS.statSync( path );
return true;
} catch( err ) {
return !(err && err.code === 'ENOENT');
}
};

View File

@ -0,0 +1,27 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Defines a regex suitable for matching FRESH versions.
@module file-contains.js
*/
// Set up a regex that matches all of the following:
//
// - FRESH
// - JRS
// - FRESCA
// - FRESH@1.0.0
// - FRESH@1.0
// - FRESH@1
// - JRS@0.16.0
// - JRS@0.16
// - JRS@0
//
// Don't use a SEMVER regex (eg, NPM's semver-regex) because a) we want to
// support partial semvers like "0" or "1.2" and b) we'll expand this later to
// support fully scoped FRESH versions.
module.exports = () => RegExp('^(FRESH|FRESCA|JRS)(?:@(\\d+(?:\\.\\d+)?(?:\\.\\d+)?))?$');

65
src/utils/html-to-wpml.js Normal file
View File

@ -0,0 +1,65 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the Markdown to WordProcessingML conversion routine.
@license MIT. Copyright (c) 2015 James Devlin / FluentDesk.
@module utils/html-to-wpml
*/
const XML = require('xml-escape');
const _ = require('underscore');
const HTML5Tokenizer = require('simple-html-tokenizer');
module.exports = function( html ) {
// Tokenize the HTML stream.
let is_bold, is_italic, is_link, link_url;
const tokens = HTML5Tokenizer.tokenize( html );
let final = (is_bold = (is_italic = (is_link = (link_url = ''))));
// Process <em>, <strong>, and <a> elements in the HTML stream, producing
// equivalent WordProcessingML that can be dumped into a <w:p> or other
// text container element.
_.each(tokens, function( tok ) {
switch (tok.type) {
case 'StartTag':
switch (tok.tagName) {
case 'p': return final += '<w:p>';
case 'strong': return is_bold = true;
case 'em': return is_italic = true;
case 'a':
is_link = true;
return link_url = tok.attributes.filter(attr => attr[0] === 'href')[0][1];
}
break;
case 'EndTag':
switch (tok.tagName) {
case 'p': return final += '</w:p>';
case 'strong': return is_bold = false;
case 'em': return is_italic = false;
case 'a': return is_link = false;
}
break;
case 'Chars':
if( tok.chars.trim().length ) {
let style = is_bold ? '<w:b/>' : '';
style += is_italic ? '<w:i/>' : '';
style += is_link ? '<w:rStyle w:val="Hyperlink"/>' : '';
return final +=
(is_link ? (`<w:hlink w:dest="${link_url}">`) : '') +
'<w:r><w:rPr>' + style + '</w:rPr><w:t>' + XML(tok.chars) +
'</w:t></w:r>' + (is_link ? '</w:hlink>' : '');
}
break;
}
});
return final;
};

20
src/utils/md2chalk.js Normal file
View File

@ -0,0 +1,20 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Inline Markdown-to-Chalk conversion routines.
@license MIT. See LICENSE.md for details.
@module utils/md2chalk
*/
const CHALK = require('chalk');
const LO = require('lodash');
module.exports = function( v, style, boldStyle ) {
boldStyle = boldStyle || 'bold';
const temp = v.replace(/\*\*(.*?)\*\*/g, LO.get( CHALK, boldStyle )('$1'));
if (style) { return LO.get( CHALK, style )(temp); } else { return temp; }
};

84
src/utils/rasterize.js Normal file
View File

@ -0,0 +1,84 @@
/* eslint-disable */
(function() {
// Exemplar script for generating documents with Phantom.js.
// https://raw.githubusercontent.com/ariya/phantomjs/master/examples/rasterize.js
// Converted to CoffeeScript by hacksalot
"use strict";
var address, output, page, pageHeight, pageWidth, size, system;
page = require('webpage').create();
system = require('system');
address = output = size = null;
if (system.args.length < 3 || system.args.length > 5) {
console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]');
console.log(' paper (pdf output) examples: "5in*7.5in", "10cm*20cm", "A4", "Letter"');
console.log(' image (png/jpg output) examples: "1920px" entire page, window width 1920px');
console.log(' "800px*600px" window, clipped to 800x600');
phantom.exit(1);
} else {
address = system.args[1];
output = system.args[2];
page.viewportSize = {
width: 600,
height: 600
};
if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") {
size = system.args[3].split('*');
page.paperSize = size.length === 2 ? {
width: size[0],
height: size[1],
margin: '0px'
} : {
format: system.args[3],
orientation: 'portrait',
margin: '1cm'
};
} else if (system.args.length > 3 && system.args[3].substr(-2) === "px") {
size = system.args[3].split('*');
if (size.length === 2) {
pageWidth = parseInt(size[0], 10);
pageHeight = parseInt(size[1], 10);
page.viewportSize = {
width: pageWidth,
height: pageHeight
};
page.clipRect = {
top: 0,
left: 0,
width: pageWidth,
height: pageHeight
};
} else {
console.log("size:", system.args[3]);
pageWidth = parseInt(system.args[3], 10);
pageHeight = parseInt(pageWidth * 3 / 4, 10); // it's as good an assumption as any
console.log("pageHeight:", pageHeight);
page.viewportSize = {
width: pageWidth,
height: pageHeight
};
}
}
if (system.args.length > 4) {
page.zoomFactor = system.args[4];
}
page.open(address, function(status) {
if (status !== 'success') {
console.log('Unable to load the address!');
phantom.exit(1);
} else {
return window.setTimeout(function() {
page.render(output);
phantom.exit();
}, 200);
}
});
}
}).call(this);
/* eslint-enable */
//# sourceMappingURL=rasterize.js.map

View File

@ -0,0 +1,20 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the ResumeDetector class.
@module utils/resume-detector
@license MIT. See LICENSE.md for details.
*/
module.exports = function( rez ) {
if (rez.meta && rez.meta.format) { //&& rez.meta.format.substr(0, 5).toUpperCase() == 'FRESH'
return 'fresh';
} else if (rez.basics) {
return 'jrs';
} else {
return 'unk';
}
};

View File

@ -0,0 +1,62 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const _ = require('underscore');
module.exports = {
/**
Removes ignored or private fields from a resume object
@returns an object with the following structure:
{
scrubbed: the processed resume object
ignoreList: an array of ignored nodes that were removed
privateList: an array of private nodes that were removed
}
*/
scrubResume(rep, opts) {
const traverse = require('traverse');
const ignoreList = [];
const privateList = [];
const includePrivates = opts && opts.private;
const scrubbed = traverse( rep ).map(function() { // [^1]
if (!this.isLeaf) {
if ((this.node.ignore === true) || (this.node.ignore === 'true')) {
ignoreList.push(this.node);
this.delete();
} else if (((this.node.private === true) || (this.node.private === 'true')) && !includePrivates) {
privateList.push(this.node);
this.delete();
}
}
if (_.isArray(this.node)) { // [^2]
this.after(function() {
this.update(_.compact(this.node));
});
}
});
return {
scrubbed,
ingoreList: ignoreList,
privateList
};
}
};
// [^1]: As of v0.6.6, the NPM traverse library has a quirk when attempting
// to remove array elements directly using traverse's `this.remove`. See:
//
// https://github.com/substack/js-traverse/issues/48
//
// [^2]: The workaround is to use traverse's 'this.delete' to nullify the value
// first, followed by removal with something like _.compact.
//
// https://github.com/substack/js-traverse/issues/48#issuecomment-142607200
//

View File

@ -0,0 +1,34 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the SafeJsonLoader class.
@module utils/safe-json-loader
@license MIT. See LICENSE.md for details.
*/
const FS = require('fs');
const SyntaxErrorEx = require('./syntax-error-ex');
module.exports = function( file ) {
const ret = { };
try {
ret.raw = FS.readFileSync( file, 'utf8' );
ret.json = JSON.parse( ret.raw );
} catch (err) {
// If we get here, either FS.readFileSync or JSON.parse failed.
// We'll return HMSTATUS.readError or HMSTATUS.parseError.
const retRaw = ret.raw && ret.raw.trim();
ret.ex = {
op: retRaw ? 'parse' : 'read',
inner:
SyntaxErrorEx.is( err )
? (new SyntaxErrorEx( err, retRaw ))
: err,
file
};
}
return ret;
};

44
src/utils/safe-spawn.js Normal file
View File

@ -0,0 +1,44 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Safe spawn utility for HackMyResume / FluentCV.
@module utils/safe-spawn
@license MIT. See LICENSE.md for details.
*/
/** Safely spawn a process synchronously or asynchronously without throwing an
exception */
module.exports = function( cmd, args, isSync, callback, param ) {
try {
// .spawnSync not available on earlier Node.js, so default to .spawn
const spawn = require('child_process')[ isSync ? 'spawnSync' : 'spawn'];
const info = spawn(cmd, args);
// Check for error depending on whether we're sync or async TODO: Promises
if (!isSync) {
info.on('error', function(err) {
if (typeof callback === 'function') {
callback(err, param);
}
});
return;
} else {
if (info.error) {
if (typeof callback === 'function') {
callback(info.error, param);
}
return {cmd, inner: info.error};
}
}
} catch (ex) {
if (typeof callback === 'function') {
callback(ex, param);
}
return ex;
}
};

View File

@ -0,0 +1,56 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Object string transformation.
@module utils/string-transformer
@license MIT. See LICENSE.md for details.
*/
const _ = require('underscore');
const moment = require('moment');
/**
Create a copy of this object in which all string fields have been run through
a transformation function (such as a Markdown filter or XML encoder).
*/
module.exports = function( ret, filt, transformer ) {
// TODO: refactor recursion
var transformStringsInObject = function( obj, filters ) {
if (!obj) { return; }
if (moment.isMoment(obj)) { return; }
if (_.isArray( obj )) {
return obj.forEach(function(elem, idx, ar) {
if ((typeof elem === 'string') || elem instanceof String) {
return ar[idx] = transformer( null, elem );
} else if (_.isObject(elem)) {
return transformStringsInObject( elem, filters );
}
});
} else if (_.isObject( obj )) {
return Object.keys( obj ).forEach(function(k) {
if (filters.length && _.contains(filters, k)) {
return;
}
const sub = obj[k];
if ((typeof sub === 'string') || sub instanceof String) {
return obj[k] = transformer( k, sub );
} else if (_.isObject( sub )) {
return transformStringsInObject( sub, filters );
}
});
}
};
Object.keys( ret ).forEach(function(member) {
if (!filt || !filt.length || !_.contains(filt, member)) {
return transformStringsInObject( ret[ member ], filt || [] );
}
});
return ret;
};

View File

@ -1,6 +1,6 @@
/**
String utility functions.
@license Copyright (c) 2015 | James M. Devlin
Definitions of string utility functions.
@module utils/string
*/
/**
@ -9,10 +9,7 @@ See: http://stackoverflow.com/a/32800728/4942583
@method isNullOrWhitespace
*/
String.isNullOrWhitespace = function( input ) {
return !input || !input.trim();
};
String.prototype.endsWith = function(suffix) {
return this.indexOf(suffix, this.length - suffix.length) !== -1;
};
String.isNullOrWhitespace = input => !input || !input.trim();
String.prototype.endsWith = function(suffix) { return this.indexOf(suffix, this.length - suffix.length) !== -1; };
String.is = val => (typeof val === 'string') || val instanceof String;

View File

@ -0,0 +1,43 @@
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the SyntaxErrorEx class.
@module utils/syntax-error-ex
@license MIT. See LICENSE.md for details.
*/
/**
Represents a SyntaxError exception with line and column info.
Collect syntax error information from the provided exception object. The
JavaScript `SyntaxError` exception isn't interpreted uniformly across environ-
ments, so we reparse on error to grab the line and column.
See: http://stackoverflow.com/q/13323356
@class SyntaxErrorEx
*/
class SyntaxErrorEx {
constructor( ex, rawData ) {
let JSONLint = require('json-lint');
const lint = JSONLint(rawData, { comments: false });
if (lint.error) { [this.line, this.col] = Array.from([lint.line, lint.character]); }
if (!lint.error) {
JSONLint = require('jsonlint');
try {
JSONLint.parse(rawData);
} catch (err) {
this.line = (/on line (\d+)/.exec(err))[1];
}
}
}
}
// Return true if the supplied parameter is a JavaScript SyntaxError
SyntaxErrorEx.is = ex => ex instanceof SyntaxError;
module.exports = SyntaxErrorEx;

89
src/verbs/analyze.js Normal file
View File

@ -0,0 +1,89 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Implementation of the 'analyze' verb for HackMyResume.
@module verbs/analyze
@license MIT. See LICENSE.md for details.
*/
const HMEVENT = require('../core/event-codes');
const HMSTATUS = require('../core/status-codes');
const _ = require('underscore');
const ResumeFactory = require('../core/resume-factory');
const Verb = require('../verbs/verb');
/** An invokable resume analysis command. */
class AnalyzeVerb extends Verb {
constructor() { super('analyze', _analyze); }
}
module.exports = AnalyzeVerb;
/** Private workhorse for the 'analyze' command. */
var _analyze = function( sources, dst, opts ) {
if (!sources || !sources.length) {
this.err(HMSTATUS.resumeNotFound, { quit: true });
return null;
}
const nlzrs = _loadInspectors();
const results = _.map(sources, function(src) {
const r = ResumeFactory.loadOne(src, { format: 'FRESH', objectify: true, inner: {
private: opts.private === true
}
}, this);
if (opts.assert && this.hasError()) { return { }; }
if (r.fluenterror) {
r.quit = opts.assert;
this.err(r.fluenterror, r);
return r;
} else {
return _analyzeOne.call(this, r, nlzrs, opts);
}
}
, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/** Analyze a single resume. */
var _analyzeOne = function( resumeObject, nlzrs ) {
const { rez } = resumeObject;
const safeFormat =
rez.meta && rez.meta.format && rez.meta.format.startsWith('FRESH')
? 'FRESH' : 'JRS';
this.stat( HMEVENT.beforeAnalyze, { fmt: safeFormat, file: resumeObject.file });
const info = _.mapObject(nlzrs, (val) => val.run(rez));
this.stat(HMEVENT.afterAnalyze, { info });
return info;
};
var _loadInspectors = () =>
({
totals: require('../inspectors/totals-inspector'),
coverage: require('../inspectors/gap-inspector'),
keywords: require('../inspectors/keyword-inspector')
})
;

441
src/verbs/build.js Normal file
View File

@ -0,0 +1,441 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Implementation of the 'build' verb for HackMyResume.
@module verbs/build
@license MIT. See LICENSE.md for details.
*/
const _ = require('underscore');
const PATH = require('path');
const MKDIRP = require('mkdirp');
const extend = require('extend');
const parsePath = require('parse-filepath');
const RConverter = require('fresh-jrs-converter');
const HMSTATUS = require('../core/status-codes');
const HMEVENT = require('../core/event-codes');
const RTYPES = {
FRESH: require('../core/fresh-resume'),
JRS: require('../core/jrs-resume')
};
const _opts = require('../core/default-options');
const FRESHTheme = require('../core/fresh-theme');
const JRSTheme = require('../core/jrs-theme');
const ResumeFactory = require('../core/resume-factory');
const _fmts = require('../core/default-formats');
const Verb = require('../verbs/verb');
//const _err = null;
//const _log = null;
let _rezObj = null;
//const build = null;
//const prep = null;
//const single = null;
//const verifyOutputs = null;
//const addFreebieFormats = null;
//const expand = null;
//const verifyTheme = null;
//const loadTheme = null;
/** An invokable resume generation command. */
class BuildVerb extends Verb {
/** Create a new build verb. */
constructor() { super('build', _build); }
}
module.exports = BuildVerb;
/**
Given a source resume in FRESH or JRS format, a destination resume path, and a
theme file, generate 0..N resumes in the desired formats.
@param src Path to the source JSON resume file: "rez/resume.json".
@param dst An array of paths to the target resume file(s).
@param opts Generation options.
*/
var _build = function( src, dst, opts ) {
let err;
if (!src || !src.length) {
this.err(HMSTATUS.resumeNotFound, {quit: true});
return null;
}
_prep.call(this, src, dst, opts);
// Load input resumes as JSON...
const sheetObjects = ResumeFactory.load(src, {
format: null, objectify: false, quit: true, inner: {
sort: _opts.sort,
private: _opts.private
}
}
, this);
// Explicit check for any resume loading errors...
const problemSheets = _.filter(sheetObjects, so => so.fluenterror);
if (problemSheets && problemSheets.length) {
problemSheets[0].quit = true; // can't go on
this.err(problemSheets[0].fluenterror, problemSheets[0]);
return null;
}
// Get the collection of raw JSON sheets
const sheets = sheetObjects.map(r => r.json);
// Load the theme...
let theme = null;
this.stat(HMEVENT.beforeTheme, { theme: _opts.theme });
try {
const tFolder = _verifyTheme.call(this, _opts.theme);
if (tFolder.fluenterror) {
tFolder.quit = true;
this.err(tFolder.fluenterror, tFolder);
return;
}
theme = (_opts.themeObj = _loadTheme(tFolder));
_addFreebieFormats(theme);
} catch (error) {
err = error;
const newEx = {
fluenterror: HMSTATUS.themeLoad,
inner: err,
attempted: _opts.theme,
quit: true
};
this.err(HMSTATUS.themeLoad, newEx);
return null;
}
this.stat(HMEVENT.afterTheme, {theme});
// Check for invalid outputs...
const inv = _verifyOutputs.call(this, dst, theme);
if (inv && inv.length) {
this.err(HMSTATUS.invalidFormat, {data: inv, theme, quit: true});
return null;
}
//# Merge input resumes, yielding a single source resume...
let rez = null;
if (sheets.length > 1) {
const isFRESH = !sheets[0].basics;
const mixed = _.any(sheets, function(s) { if (isFRESH) { return s.basics; } else { return !s.basics; } });
this.stat(HMEVENT.beforeMerge, { f: _.clone(sheetObjects), mixed });
if (mixed) {
this.err(HMSTATUS.mixedMerge);
}
rez = _.reduceRight(sheets, ( a, b ) => extend( true, b, a ));
this.stat(HMEVENT.afterMerge, { r: rez });
} else {
rez = sheets[0];
}
// Convert the merged source resume to the theme's format, if necessary..
const orgFormat = rez.basics ? 'JRS' : 'FRESH';
const toFormat = theme.render ? 'JRS' : 'FRESH';
if (toFormat !== orgFormat) {
this.stat(HMEVENT.beforeInlineConvert);
rez = RConverter[ `to${toFormat}` ]( rez );
this.stat(HMEVENT.afterInlineConvert, { file: sheetObjects[0].file, fmt: toFormat });
}
// Announce the theme
this.stat(HMEVENT.applyTheme, {r: rez, theme});
// Load the resume into a FRESHResume or JRSResume object
_rezObj = new (RTYPES[ toFormat ])().parseJSON( rez, {private: _opts.private} );
// Expand output resumes...
const targets = _expand(dst, theme);
// Run the transformation!
_.each(targets, function(t) {
if (this.hasError() && opts.assert) { return { }; }
t.final = _single.call(this, t, theme, targets);
if (t.final != null ? t.final.fluenterror : undefined) {
t.final.quit = opts.assert;
this.err(t.final.fluenterror, t.final);
}
}
, this);
const results = {
sheet: _rezObj,
targets,
processed: targets
};
if (this.hasError() && !opts.assert) {
this.reject(results);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/**
Prepare for a BUILD run.
*/
var _prep = function( src, dst, opts ) {
// Cherry-pick options //_opts = extend( true, _opts, opts );
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim()) || 'modern';
_opts.prettify = opts.prettify === true;
_opts.private = opts.private === true;
_opts.noescape = opts.noescape === true;
_opts.css = opts.css;
_opts.pdf = opts.pdf;
_opts.wrap = opts.wrap || 60;
_opts.stitles = opts.sectionTitles;
_opts.tips = opts.tips;
_opts.errHandler = opts.errHandler;
_opts.noTips = opts.noTips;
_opts.debug = opts.debug;
_opts.sort = opts.sort;
_opts.wkhtmltopdf = opts.wkhtmltopdf;
const that = this;
// Set up callbacks for internal generators
_opts.onTransform = function(info) {
that.stat(HMEVENT.afterTransform, info);
};
_opts.beforeWrite = function(info) {
that.stat(HMEVENT.beforeWrite, info);
};
_opts.afterWrite = function(info) {
that.stat(HMEVENT.afterWrite, info);
};
// If two or more files are passed to the GENERATE command and the TO
// keyword is omitted, the last file specifies the output file.
( (src.length > 1) && ( !dst || !dst.length ) ) && dst.push( src.pop() );
};
/**
Generate a single target resume such as "out/rez.html" or "out/rez.doc".
TODO: Refactor.
@param targInfo Information for the target resume.
@param theme A FRESHTheme or JRSTheme object.
*/
var _single = function( targInfo, theme, finished ) {
let ret = null;
let ex = null;
const f = targInfo.file;
try {
if (!targInfo.fmt) {
return { };
}
let theFormat = null;
this.stat(HMEVENT.beforeGenerate, {
fmt: targInfo.fmt.outFormat,
file: PATH.relative(process.cwd(), f)
}
);
_opts.targets = finished;
// If targInfo.fmt.files exists, this format is backed by a document.
// Fluent/FRESH themes are handled here.
if (targInfo.fmt.files && targInfo.fmt.files.length) {
theFormat = _fmts.filter( fmt => fmt.name === targInfo.fmt.outFormat)[0];
MKDIRP.sync(PATH.dirname( f ));
ret = theFormat.gen.generate(_rezObj, f, _opts);
// Otherwise this is an ad-hoc format (JSON, YML, or PNG) that every theme
// gets "for free".
} else {
theFormat = _fmts.filter( fmt => fmt.name === targInfo.fmt.outFormat)[0];
const outFolder = PATH.dirname(f);
MKDIRP.sync(outFolder); // Ensure dest folder exists;
ret = theFormat.gen.generate(_rezObj, f, _opts);
}
} catch (e) {
ex = e;
}
this.stat(HMEVENT.afterGenerate, {
fmt: targInfo.fmt.outFormat,
file: PATH.relative(process.cwd(), f),
error: ex
}
);
if (ex) {
if (ex.fluenterror) {
ret = ex;
} else {
ret = {fluenterror: HMSTATUS.generateError, inner: ex};
}
}
return ret;
};
/** Ensure that user-specified outputs/targets are valid. */
var _verifyOutputs = function( targets, theme ) {
this.stat(HMEVENT.verifyOutputs, {targets, theme});
return _.reject(targets.map( function( t ) {
const pathInfo = parsePath(t);
return {format: pathInfo.extname.substr(1)}; }),
t => (t.format === 'all') || theme.hasFormat( t.format ));
};
/**
Reinforce the chosen theme with "freebie" formats provided by HackMyResume.
A "freebie" format is an output format such as JSON, YML, or PNG that can be
generated directly from the resume model or from one of the theme's declared
output formats. For example, the PNG format can be generated for any theme
that declares an HTML format; the theme doesn't have to provide an explicit
PNG template.
@param theTheme A FRESHTheme or JRSTheme object.
*/
var _addFreebieFormats = function( theTheme ) {
// Add freebie formats (JSON, YAML, PNG) every theme gets...
// Add HTML-driven PNG only if the theme has an HTML format.
theTheme.formats.json = theTheme.formats.json || {
freebie: true, title: 'json', outFormat: 'json', pre: 'json',
ext: 'json', path: null, data: null
};
theTheme.formats.yml = theTheme.formats.yml || {
freebie: true, title: 'yaml', outFormat: 'yml', pre: 'yml',
ext: 'yml', path: null, data: null
};
if (theTheme.formats.html && !theTheme.formats.png) {
theTheme.formats.png = {
freebie: true, title: 'png', outFormat: 'png',
ext: 'yml', path: null, data: null
};
}
};
/**
Expand output files. For example, "foo.all" should be expanded to
["foo.html", "foo.doc", "foo.pdf", "etc"].
@param dst An array of output files as specified by the user.
@param theTheme A FRESHTheme or JRSTheme object.
*/
var _expand = function( dst, theTheme ) {
// Set up the destination collection. It's either the array of files passed
// by the user or 'out/resume.all' if no targets were specified.
const destColl = (dst && dst.length && dst) || [PATH.normalize('out/resume.all')];
// Assemble an array of expanded target files... (can't use map() here)
const targets = [];
destColl.forEach(function(t) {
const to = PATH.resolve(t);
const pa = parsePath(to);
const fmat = pa.extname || '.all';
return targets.push.apply( targets,
fmat === '.all'
? Object.keys( theTheme.formats ).map( function( k ) {
const z = theTheme.formats[k];
return { file: to.replace( /all$/g, z.outFormat ), fmt: z };
})
: [{ file: to, fmt: theTheme.getFormat( fmat.slice(1) ) }]
);
});
return targets;
};
/**
Verify the specified theme name/path.
*/
var _verifyTheme = function( themeNameOrPath ) {
// First, see if this is one of the predefined FRESH themes. There are only a
// handful of these, but they may change over time, so we need to query
// the official source of truth: the fresh-themes repository, which mounts the
// themes conveniently by name to the module object, and which is embedded
// locally inside the HackMyResume installation.
let tFolder;
const themesObj = require('fresh-themes');
if (_.has(themesObj.themes, themeNameOrPath)) {
tFolder = PATH.join(
parsePath( require.resolve('fresh-themes') ).dirname,
'/themes/',
themeNameOrPath
);
} else {
// Otherwsie it's a path to an arbitrary FRESH or JRS theme sitting somewhere
// on the user's system (or, in the future, at a URI).
tFolder = PATH.resolve(themeNameOrPath);
}
// In either case, make sure the theme folder exists
const exists = require('path-exists').sync;
if (exists(tFolder)) {
return tFolder;
} else {
return {fluenterror: HMSTATUS.themeNotFound, data: _opts.theme};
}
};
/**
Load the specified theme, which could be either a FRESH theme or a JSON Resume
theme (or both).
*/
var _loadTheme = function( tFolder ) {
const themeJsonPath = PATH.join(tFolder, 'theme.json'); // [^1]
const exists = require('path-exists').sync;
// Create a FRESH or JRS theme object
const theTheme =
exists(themeJsonPath)
? new FRESHTheme().open(tFolder)
: new JRSTheme().open(tFolder);
// Cache the theme object
_opts.themeObj = theTheme;
return theTheme;
};
// FOOTNOTES
// ------------------------------------------------------------------------------
// [^1] We don't know ahead of time whether this is a FRESH or JRS theme.
// However, all FRESH themes have a theme.json file, so we'll use that as a
// canary for now, as an interim solution.
//
// Unfortunately, with the exception of FRESH's theme.json, both FRESH and
// JRS themes are free-form and don't have a ton of reliable distinguishing
// marks, which makes a simple task like ad hoc theme detection harder than
// it should be to do cleanly.
//
// Another complicating factor is that it's possible for a theme to be BOTH.
// That is, a single set of theme files can serve as a FRESH theme -and- a
// JRS theme.
//
// TODO: The most robust way to deal with all these issues is with a strong
// theme validator. If a theme structure validates as a particular theme
// type, then for all intents and purposes, it IS a theme of that type.

171
src/verbs/convert.js Normal file
View File

@ -0,0 +1,171 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Implementation of the 'convert' verb for HackMyResume.
@module verbs/convert
@license MIT. See LICENSE.md for details.
*/
const ResumeFactory = require('../core/resume-factory');
const Verb = require('../verbs/verb');
const HMSTATUS = require('../core/status-codes');
const _ = require('underscore');
const HMEVENT = require('../core/event-codes');
class ConvertVerb extends Verb {
constructor() { super('convert', _convert); }
}
module.exports = ConvertVerb;
/** Private workhorse method. Convert 0..N resumes between FRESH and JRS
formats. */
var _convert = function( srcs, dst, opts ) {
// If no source resumes are specified, error out
let fmtUp;
if (!srcs || !srcs.length) {
this.err(HMSTATUS.resumeNotFound, { quit: true });
return null;
}
// If no destination resumes are specified, error out except for the special
// case of two resumes:
// hackmyresume CONVERT r1.json r2.json
if (!dst || !dst.length) {
if (srcs.length === 1) {
this.err(HMSTATUS.inputOutputParity, { quit: true });
} else if (srcs.length === 2) {
dst = dst || []; dst.push( srcs.pop() );
} else {
this.err(HMSTATUS.inputOutputParity, { quit: true });
}
}
// Different number of source and dest resumes? Error out.
if (srcs && dst && srcs.length && dst.length && (srcs.length !== dst.length)) {
this.err(HMSTATUS.inputOutputParity, { quit: true });
}
// Validate the destination format (if specified)
//const targetVer = null;
if (opts.format) {
fmtUp = opts.format.trim().toUpperCase();
if (!_.contains(['FRESH','FRESCA','JRS','JRS@1','JRS@edge'], fmtUp)) {
this.err(HMSTATUS.invalidSchemaVersion, {data: opts.format.trim(), quit: true});
}
}
// freshVerRegex = require '../utils/fresh-version-regex'
// matches = fmtUp.match freshVerRegex()
// # null
// # [ 'JRS@1.0', 'JRS', '1.0', index: 0, input: 'FRESH' ]
// # [ 'FRESH', 'FRESH', undefined, index: 0, input: 'FRESH' ]
// if not matches
// @err HMSTATUS.invalidSchemaVersion, data: opts.format.trim(), quit: true
// targetSchema = matches[1]
// targetVer = matches[2] || '1'
// If any errors have occurred this early, we're done.
if (this.hasError()) {
this.reject(this.errorCode);
return null;
}
// Map each source resume to the converted destination resume
const results = _.map(srcs, function( src, idx ) {
// Convert each resume in turn
const r = _convertOne.call(this, src, dst, idx, fmtUp);
// Handle conversion errors
if (r.fluenterror) {
r.quit = opts.assert;
this.err(r.fluenterror, r);
}
return r;
}
, this);
if (this.hasError() && !opts.assert) {
this.reject(results);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/** Private workhorse method. Convert a single resume. */
var _convertOne = function(src, dst, idx, targetSchema) {
// Load the resume
const rinfo = ResumeFactory.loadOne(src, {
format: null,
objectify: true,
inner: {
privatize: false
}
}
);
// If a load error occurs, report it and move on to the next file (if any)
if (rinfo.fluenterror) {
this.stat(HMEVENT.beforeConvert, {
srcFile: src, //rinfo.file
srcFmt: '???',
dstFile: dst[idx],
dstFmt: '???',
error: true
}
);
//@err rinfo.fluenterror, rinfo
return rinfo;
}
// Determine the resume's SOURCE format
// TODO: replace with detector component
const { rez } = rinfo;
let srcFmt = '';
if (rez.meta && rez.meta.format) { //&& rez.meta.format.substr(0, 5).toUpperCase() == 'FRESH'
srcFmt = 'FRESH';
} else if (rez.basics) {
srcFmt = 'JRS';
} else {
rinfo.fluenterror = HMSTATUS.unknownSchema;
return rinfo;
}
// Determine the TARGET format for the conversion
const targetFormat = targetSchema || (srcFmt === 'JRS' ? 'FRESH' : 'JRS');
// Fire the beforeConvert event
this.stat(HMEVENT.beforeConvert, {
srcFile: rinfo.file,
srcFmt,
dstFile: dst[idx],
dstFmt: targetFormat
}
);
// Save it to the destination format
try {
rez.saveAs(dst[idx], targetFormat);
} catch (err) {
if (err.badVer) {
return {fluenterror: HMSTATUS.invalidSchemaVersion, quit: true, data: err.badVer};
}
}
return rez;
};

84
src/verbs/create.js Normal file
View File

@ -0,0 +1,84 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Implementation of the 'create' verb for HackMyResume.
@module verbs/create
@license MIT. See LICENSE.md for details.
*/
const MKDIRP = require('mkdirp');
const PATH = require('path');
const Verb = require('../verbs/verb');
const _ = require('underscore');
const HMSTATUS = require('../core/status-codes');
const HMEVENT = require('../core/event-codes');
class CreateVerb extends Verb {
constructor() { super('new', _create); }
}
module.exports = CreateVerb;
/** Create a new empty resume in either FRESH or JRS format. */
var _create = function( src, dst, opts ) {
if (!src || !src.length) {
this.err(HMSTATUS.createNameMissing, { quit: true });
return null;
}
const results = _.map(src, function( t ) {
if (opts.assert && this.hasError()) { return { }; }
const r = _createOne.call(this, t, opts);
if (r.fluenterror) {
r.quit = opts.assert;
this.err(r.fluenterror, r);
}
return r;
}
, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/** Create a single new resume */
var _createOne = function( t, opts ) {
let ret, safeFmt;
try {
ret = null;
safeFmt = opts.format.toUpperCase();
this.stat(HMEVENT.beforeCreate, { fmt: safeFmt, file: t });
MKDIRP.sync(PATH.dirname( t )); // Ensure dest folder exists;
const RezClass = require(`../core/${safeFmt.toLowerCase()}-resume`);
const newRez = RezClass.default();
newRez.save(t);
ret = newRez;
} catch (err) {
ret = {
fluenterror: HMSTATUS.createError,
inner: err
};
}
finally {
this.stat(HMEVENT.afterCreate, { fmt: safeFmt, file: t, isError: ret.fluenterror });
}
return ret;
};

103
src/verbs/peek.js Normal file
View File

@ -0,0 +1,103 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Implementation of the 'peek' verb for HackMyResume.
@module verbs/peek
@license MIT. See LICENSE.md for details.
*/
const Verb = require('../verbs/verb');
const _ = require('underscore');
const __ = require('lodash');
const safeLoadJSON = require('../utils/safe-json-loader');
const HMSTATUS = require('../core/status-codes');
const HMEVENT = require('../core/event-codes');
class PeekVerb extends Verb {
constructor() { super('peek', _peek); }
}
module.exports = PeekVerb;
/** Peek at a resume, resume section, or resume field. */
var _peek = function( src, dst, opts ) {
if (!src || !src.length) {
this.err(HMSTATUS.resumeNotFound, { quit: true });
return null;
}
const objPath = (dst && dst[0]) || '';
const results = _.map(src, function( t ) {
if (opts.assert && this.hasError()) { return { }; }
const tgt = _peekOne.call(this, t, objPath);
if (tgt.error) {
this.setError(tgt.error.fluenterror, tgt.error);
}
//tgt.error.quit = opts.assert
//@err tgt.error.fluenterror, tgt.error
return tgt;
}
, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
/** Peek at a single resume, resume section, or resume field. */
var _peekOne = function( t, objPath ) {
this.stat(HMEVENT.beforePeek, { file: t, target: objPath });
// Load the input file JSON 1st
const obj = safeLoadJSON(t);
// Fetch the requested object path (or the entire file)
let tgt = null;
if (!obj.ex) {
tgt = objPath ? __.get(obj.json, objPath) : obj.json;
}
//# safeLoadJSON can only return a READ error or a PARSE error
let pkgError = null;
if (obj.ex) {
const errCode = obj.ex.op === 'parse' ? HMSTATUS.parseError : HMSTATUS.readError;
if (errCode === HMSTATUS.readError) {
obj.ex.quiet = true;
}
pkgError = {fluenterror: errCode, inner: obj.ex};
}
// Fire the 'afterPeek' event with collected info
this.stat(HMEVENT.afterPeek, {
file: t,
requested: objPath,
target: obj.ex ? undefined : tgt,
error: pkgError
}
);
return {
val: obj.ex ? undefined : tgt,
error: pkgError
};
};

113
src/verbs/validate.js Normal file
View File

@ -0,0 +1,113 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Implementation of the 'validate' verb for HackMyResume.
@module verbs/validate
@license MIT. See LICENSE.md for details.
*/
const Verb = require('../verbs/verb');
const HMSTATUS = require('../core/status-codes');
const HMEVENT = require('../core/event-codes');
const _ = require('underscore');
const safeLoadJSON = require('../utils/safe-json-loader');
/** An invokable resume validation command. */
class ValidateVerb extends Verb {
constructor() { super('validate', _validate); }
}
// Validate 1 to N resumes in FRESH or JSON Resume format.
var _validate = function(sources, unused, opts) {
if (!sources || !sources.length) {
this.err(HMSTATUS.resumeNotFoundAlt, {quit: true});
return null;
}
const validator = require('is-my-json-valid');
const schemas = {
fresh: require('fresh-resume-schema'),
jars: require('../core/resume.json')
};
const results = _.map(sources, function(t) {
const r = _validateOne.call(this, t, validator, schemas, opts);
if (r.error) { this.err(r.error.fluenterror, r.error); }
return r;
}
, this);
if (this.hasError() && !opts.assert) {
this.reject(this.errorCode);
} else if (!this.hasError()) {
this.resolve(results);
}
return results;
};
module.exports = ValidateVerb;
/**
Validate a single resume.
@returns {
file: <fileName>,
isValid: <validFlag>,
status: <validationStatus>,
violations: <validationErrors>,
schema: <schemaType>,
error: <errorObject>
}
*/
var _validateOne = function(t, validator, schemas) {
const ret = {file: t, isValid: false, status: 'unknown', schema: '-----'};
try {
// Read and parse the resume JSON. Won't throw.
const obj = safeLoadJSON(t);
// If success, validate the resume against the schema
if (!obj.ex) {
if (obj.json.basics) { ret.schema = 'jars'; } else { ret.schema = 'fresh'; }
const validate = validator(schemas[ ret.schema ], // Note [1]
{formats: { date: /^\d{4}(?:-(?:0[0-9]{1}|1[0-2]{1})(?:-[0-9]{2})?)?$/ }});
ret.isValid = validate(obj.json);
ret.status = ret.isValid ? 'valid' : 'invalid';
if (!ret.isValid) { ret.violations = validate.errors; }
// If failure, package JSON read/parse errors
} else {
let errCode;
if (obj.ex.op === 'parse') {
errCode = HMSTATUS.parseError;
ret.status = 'broken';
} else {
errCode = HMSTATUS.readError;
ret.status = 'missing';
}
ret.error = {
fluenterror: errCode,
inner: obj.ex.inner,
quiet: errCode === HMSTATUS.readError
};
}
} catch (err) {
// Package any unexpected exceptions
ret.error = {fluenterror: HMSTATUS.validateError, inner: err};
}
this.stat(HMEVENT.afterValidate, ret);
return ret;
};

115
src/verbs/verb.js Normal file
View File

@ -0,0 +1,115 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/**
Definition of the Verb class.
@module verbs/verb
@license MIT. See LICENSE.md for details.
*/
const EVENTS = require('events');
const HMEVENT = require('../core/event-codes');
const Promise = require('pinkie-promise');
/**
An abstract invokable verb.
Provides base class functionality for verbs. Provide common services such as
error handling, event management, and promise support.
@class Verb
*/
class Verb {
/** Constructor. Automatically called at creation. */
constructor( moniker, workhorse ) {
this.moniker = moniker;
this.workhorse = workhorse;
this.emitter = new EVENTS.EventEmitter();
}
/** Invoke the command. */
invoke() {
// Sent the 'begin' notification for this verb
this.stat(HMEVENT.begin, {cmd: this.moniker});
// Prepare command arguments
const argsArray = Array.prototype.slice.call(arguments);
// Create a promise for this verb instance
const that = this;
return this.promise = new Promise(function(res, rej) {
that.resolve = res;
that.reject = rej;
that.workhorse.apply(that, argsArray);
});
}
/** Forward subscriptions to the event emitter. */
on() { return this.emitter.on.apply(this.emitter, arguments); }
/** Fire an arbitrary event, scoped to "hmr:". */
fire(evtName, payload) {
payload = payload || { };
payload.cmd = this.moniker;
this.emitter.emit(`hmr:${evtName}`, payload);
return true;
}
/** Handle an error condition. */
err( errorCode, payload, hot ) {
payload = payload || { };
payload.sub = (payload.fluenterror = errorCode);
payload.throw = hot;
this.setError(errorCode, payload);
if (payload.quit) {
this.reject(errorCode);
}
this.fire('error', payload);
if (hot) {
throw payload;
}
return true;
}
/** Fire the 'hmr:status' error event. */
stat( subEvent, payload ) {
payload = payload || { };
payload.sub = subEvent;
this.fire('status', payload);
return true;
}
/** Has an error occurred during this verb invocation? */
hasError() { return this.errorCode || this.errorObj; }
/** Associate error info with the invocation. */
setError( code, obj ) {
this.errorCode = code;
this.errorObj = obj;
}
}
module.exports = Verb;

1
test/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
sandbox/

12
test/all.js Normal file
View File

@ -0,0 +1,12 @@
var MKDIRP = require('mkdirp');
var PATH = require('path');
MKDIRP( PATH.join( __dirname, '/sandbox' ) );
require('./scripts/test-cli');
require('./scripts/test-fresh-sheet');
require('./scripts/test-jrs-sheet');
require('./scripts/test-themes');
require('./scripts/test-verbs');
require('./scripts/test-output');
require('./scripts/test-dates');

View File

@ -0,0 +1,204 @@
* {
box-sizing: border-box;
}
main {
display: block;
}
body {
font-family: 'Helvetica Neue', 'Helvetica', 'Segoe UI', 'Calibri', 'sans-serif';
font-size: 15px;
color: #333;
line-height: 1.42857143;
background-color: #F0F0F0;
margin: 0;
padding: 0;
}
/* Typical page borders are awkward when rendered to PDF. */
body.pdf {
background-color: #FFFFFF;
}
/* Adobe or wkhtmltopdf has issues with the <main> tag, so we use <div> for
the PDF case, <main> for the HTML case, and style both via an ID. */
#main {
background-color: #FFF;
margin: 10px;
padding: 10px;
border: 1px solid #E6E6E6;
}
body.pdf > #main {
border: none;
}
#container > header {
padding-top: 6em;
padding-bottom: 1em;
}
body.pdf #container > header {
padding: 0;
}
#main > #container > section {
margin-left: 5em;
position: relative;
display: block;
}
span.fa
{
position: absolute;
top: 4px;
left: -50px;
font-size: 30px;
color: #BFC1C3;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #EEE;
}
.tenure, .keywords {
font-size: 75%;
}
h1 {
margin: 0;
font-size: 46px;
}
h2 {
font-size: 24px;
color: #BFC1C3;
text-transform: uppercase;
font-weight: normal;
}
h3 {
margin-bottom: 0;
font-size: 18px;
}
a, a:visited {
color: #428BCA;
text-decoration: none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
.defunct {
color: #989898;
font-weight: bold;
}
#summary > p > strong {
font-size: 1.25em;
}
.label-keyword {
display: inline-block;
background: #7eb0db;
color: white;
font-size: 0.9em;
padding: 5px;
border: 1px solid #357ebd;
border-radius: 5px;
margin-top: 2px;
}
.card-skills {
position: relative;
}
.card-nested {
min-height: 0;
margin-bottom: 10px;
border-width: 1px 0 0 0;
}
.card {
background: #FFF;
border-radius: 3px;
padding: 10px;
}
.skill-level {
border-radius: 3px;
position: absolute;
top: 10px;
bottom: 10px;
left: 0;
width: 10px;
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5);
}
.skill-level .skill-progress {
position: absolute;
border-radius: 3px;
bottom: 0;
width: 100%;
-webkit-transition: height 1s ease;
}
.skill-level .skill-progress.beginner {
height: 50%;
background: #e74c3c;
}
.skill-level .skill-progress.intermediate {
height: 70%;
background: #f1c40f;
}
.skill-level .skill-progress.advanced {
height: 80%;
background: #428bca;
}
.skill-level .skill-progress.master {
height: 95%;
background: #5cb85c;
}
.skill-info {
margin-left: 10px;
}
@media (max-width: 480px) {
.skill-info {
margin-left: 20px;
}
}
.list-unstyled {
padding-left: 0;
list-style: none;
}
.card-skills {
position: relative;
}
.space-top {
margin-top: 10px;
}
#container {
max-width: 800px;
margin: 0 auto;
}
#elevator-pitch {
text-align: center;
font-size: 24px;
color: #BFC1C3;
text-transform: uppercase;
font-weight: normal;
}
.res-label {
font-style: italic;
}

View File

@ -0,0 +1,203 @@
* {
box-sizing: border-box;
}
main {
display: block;
}
body {
font-family: 'Helvetica Neue', 'Helvetica', 'Segoe UI', 'Calibri', 'sans-serif';
font-size: 15px;
color: #333;
line-height: 1.42857143;
background-color: #F0F0F0;
margin: 0;
padding: 0;
}
/* Typical page borders are awkward when rendered to PDF. */
body {
background-color: #FFFFFF;
}
/* Adobe or wkhtmltopdf has issues with the <main> tag, so we use <div> for
the PDF case, <main> for the HTML case, and style both via an ID. */
#main {
background-color: #FFF;
margin: 10px;
padding: 10px;
border: 1px solid #E6E6E6;
}
body > #main {
border: none;
}
#container > header {
padding-top: 6em;
padding-bottom: 1em;
}
body.pdf #container > header {
padding: 0;
}
#main > #container > section {
margin-left: 5em;
position: relative;
display: block;
}
span.fa
{
position: absolute;
top: 4px;
left: -50px;
font-size: 30px;
color: #BFC1C3;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #EEE;
}
.tenure, .keywords {
font-size: 75%;
}
h1 {
margin: 0;
font-size: 46px;
}
h2 {
font-size: 24px;
color: #BFC1C3;
text-transform: uppercase;
font-weight: normal;
}
h3 {
margin-bottom: 0;
font-size: 18px;
}
a, a:visited {
color: #428BCA;
text-decoration: none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
.defunct {
color: #989898;
font-weight: bold;
}
#summary > p > strong {
font-size: 1.25em;
}
.label-keyword {
display: inline-block;
background: #7eb0db;
color: white;
font-size: 0.9em;
padding: 5px;
border: 1px solid #357ebd;
border-radius: 5px;
margin-top: 2px;
}
.card-skills {
position: relative;
}
.card-nested {
min-height: 0;
margin-bottom: 10px;
border-width: 1px 0 0 0;
}
.card {
background: #FFF;
border-radius: 3px;
padding: 10px;
}
.skill-level {
border-radius: 3px;
position: absolute;
top: 10px;
bottom: 10px;
left: 0;
width: 10px;
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5);
}
.skill-level .skill-progress {
position: absolute;
border-radius: 3px;
bottom: 0;
width: 100%;
-webkit-transition: height 1s ease;
}
.skill-level .skill-progress.beginner {
height: 50%;
background: #e74c3c;
}
.skill-level .skill-progress.intermediate {
height: 70%;
background: #f1c40f;
}
.skill-level .skill-progress.advanced {
height: 80%;
background: #428bca;
}
.skill-level .skill-progress.master {
height: 95%;
background: #5cb85c;
}
.skill-info {
margin-left: 10px;
}
@media (max-width: 480px) {
.skill-info {
margin-left: 20px;
}
}
.list-unstyled {
padding-left: 0;
list-style: none;
}
.card-skills {
position: relative;
}
.space-top {
margin-top: 10px;
}
#container {
max-width: 800px;
margin: 0 auto;
}
#elevator-pitch {
text-align: center;
font-size: 24px;
color: #BFC1C3;
text-transform: uppercase;
font-weight: normal;
}
.res-label {
font-style: italic;
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More