resume-cli 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,7 +24,7 @@ npm install -g resume-cli
24
24
  |---|---|
25
25
  | init | Initialize a `resume.json` file. |
26
26
  | validate | Schema validation test your `resume.json`. |
27
- | export path/to/file.html | Export to `.html`. |
27
+ | export path/to/file.html | Export to `.html`, `.pdf`, `.md` or `.txt`. |
28
28
  | serve | Serve resume at `http://localhost:4000/`. |
29
29
 
30
30
  ### `resume --help`
@@ -43,9 +43,30 @@ Validates your `resume.json` against our schema to ensure it complies with the s
43
43
 
44
44
  ### `resume export [fileName]`
45
45
 
46
- Exports your resume in a stylized HTML or PDF format.
46
+ Exports your resume to one of four formats:
47
47
 
48
- A list of available themes can be found here:
48
+ - `.html` / `.pdf` stylized output rendered through a theme.
49
+ - `.md` — clean Markdown (one heading per section). No theme required.
50
+ - `.txt` — readable plain text. No theme required.
51
+
52
+ The format is inferred from the file extension, or set it explicitly with
53
+ `--format`:
54
+
55
+ ```
56
+ resume export resume.md # Markdown (inferred)
57
+ resume export resume.txt --format text # plain text (explicit)
58
+ resume export resume.html --theme even # themed HTML
59
+ resume export resume.pdf --theme even # themed PDF
60
+ ```
61
+
62
+ `--format` accepts `html`, `pdf`, `markdown` (or `md`) and `text` (or `txt`).
63
+
64
+ The Markdown and text formats render every JSON Resume section that is present
65
+ (basics, work, volunteer, education, awards, publications, skills, languages,
66
+ interests, references, projects) and skip any that are missing — so no theme
67
+ install is needed.
68
+
69
+ A list of available themes (for `.html` / `.pdf`) can be found here:
49
70
  https://jsonresume.org/themes/
50
71
 
51
72
  Please npm install the theme you wish to use before attempting to export it.
@@ -53,7 +74,7 @@ Please npm install the theme you wish to use before attempting to export it.
53
74
  Options:
54
75
 
55
76
  - `--format <file type>` Example: `--format pdf`
56
- - `--theme <name>` Example: `--theme even`
77
+ - `--theme <name>` Example: `--theme even` (only used for `.html` / `.pdf`)
57
78
 
58
79
  ### `resume serve`
59
80
 
@@ -11,6 +11,29 @@ const btoa = require('btoa');
11
11
  const {
12
12
  ThemeNotFoundError
13
13
  } = require('./theme-errors');
14
+ const toMarkdown = require('./formatters/markdown');
15
+ const toText = require('./formatters/text');
16
+
17
+ // Theme-less formatters keyed by their normalized format. The value pairs the
18
+ // pure renderer with the file extension used on disk.
19
+ const THEMELESS_FORMATTERS = {
20
+ md: {
21
+ render: toMarkdown,
22
+ ext: '.md'
23
+ },
24
+ markdown: {
25
+ render: toMarkdown,
26
+ ext: '.md'
27
+ },
28
+ txt: {
29
+ render: toText,
30
+ ext: '.txt'
31
+ },
32
+ text: {
33
+ render: toText,
34
+ ext: '.txt'
35
+ }
36
+ };
14
37
  module.exports = ({
15
38
  resume: resumeJson,
16
39
  fileName,
@@ -24,6 +47,16 @@ module.exports = ({
24
47
  const fileNameAndFormat = getFileNameAndFormat(fileName, format);
25
48
  fileName = fileNameAndFormat.fileName;
26
49
  const fileFormatToUse = fileNameAndFormat.fileFormatToUse;
50
+ const themeless = THEMELESS_FORMATTERS[(fileFormatToUse || '').toLowerCase()];
51
+ if (themeless) {
52
+ createThemeless(resumeJson, fileName, themeless, error => {
53
+ if (error) {
54
+ console.error(error, '`createThemeless` errored out');
55
+ }
56
+ callback(error, fileName, themeless.ext);
57
+ });
58
+ return;
59
+ }
27
60
  const formatToUse = '.' + fileFormatToUse;
28
61
  if (formatToUse === '.html') {
29
62
  createHtml(resumeJson, fileName, theme, formatToUse, error => {
@@ -88,6 +121,17 @@ async function createHtml(resumeJson, fileName, themePath, format, callback) {
88
121
  stream.close(callback);
89
122
  });
90
123
  }
124
+ // Render a resume with a pure (theme-less) formatter and write it to disk.
125
+ async function createThemeless(resumeJson, fileName, formatter, callback) {
126
+ try {
127
+ const output = formatter.render(resumeJson);
128
+ const pathToStream = path.resolve(process.cwd(), fileName + formatter.ext);
129
+ await writeFile(pathToStream, output);
130
+ callback(null);
131
+ } catch (err) {
132
+ callback(err);
133
+ }
134
+ }
91
135
  const createPdf = (resumeJson, fileName, theme, format, callback) => {
92
136
  (async () => {
93
137
  const themePkg = getThemePkg(theme);
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+
3
+ // Pure Markdown formatter for a JSON Resume. No external theme required.
4
+ // Renders every JSON Resume schema section that is present, and silently
5
+ // skips sections that are missing or empty so partial resumes work too.
6
+
7
+ const {
8
+ isNonEmptyArray,
9
+ dateRange,
10
+ contactParts,
11
+ section
12
+ } = require('./utils');
13
+ const link = (label, url) => url ? `[${label}](${url})` : label;
14
+ const sec = (title, items, renderItem) => section([`## ${title}`], items, renderItem);
15
+ const highlights = items => isNonEmptyArray(items) ? items.map(h => `- ${h}`) : [];
16
+ const csv = keywords => isNonEmptyArray(keywords) ? [keywords.join(', ')] : [];
17
+ const italic = text => text ? [`*${text}*`] : [];
18
+ const basics = resume => {
19
+ const b = resume.basics;
20
+ if (!b) {
21
+ return [];
22
+ }
23
+ const lines = [];
24
+ if (b.name) {
25
+ lines.push(`# ${b.name}`);
26
+ }
27
+ if (b.label) {
28
+ lines.push(`> ${b.label}`);
29
+ }
30
+ const contact = contactParts(b);
31
+ if (contact.length) {
32
+ lines.push(contact.join(' | '));
33
+ }
34
+ if (isNonEmptyArray(b.profiles)) {
35
+ const profiles = b.profiles.map(p => link(p.network || p.username || p.url, p.url)).filter(Boolean);
36
+ if (profiles.length) {
37
+ lines.push(profiles.join(' | '));
38
+ }
39
+ }
40
+ if (b.summary) {
41
+ lines.push('', b.summary);
42
+ }
43
+ return lines;
44
+ };
45
+ const work = resume => sec('Work', resume.work, w => {
46
+ const heading = [w.position, w.name || w.company].filter(Boolean).join(', ');
47
+ const lines = [`### ${link(heading || 'Role', w.url)}`];
48
+ lines.push(...italic(dateRange(w.startDate, w.endDate)));
49
+ if (w.summary) {
50
+ lines.push('', w.summary);
51
+ }
52
+ return lines.concat(highlights(w.highlights));
53
+ });
54
+ const volunteer = resume => sec('Volunteer', resume.volunteer, v => {
55
+ const heading = [v.position, v.organization].filter(Boolean).join(', ');
56
+ const lines = [`### ${link(heading || 'Volunteer', v.url)}`];
57
+ lines.push(...italic(dateRange(v.startDate, v.endDate)));
58
+ if (v.summary) {
59
+ lines.push('', v.summary);
60
+ }
61
+ return lines.concat(highlights(v.highlights));
62
+ });
63
+ const education = resume => sec('Education', resume.education, e => {
64
+ const study = [e.studyType, e.area].filter(Boolean).join(', ');
65
+ const title = [e.institution, study].filter(Boolean).join(' — ');
66
+ const lines = [`### ${link(title || 'Education', e.url)}`];
67
+ lines.push(...italic(dateRange(e.startDate, e.endDate)));
68
+ if (e.score) {
69
+ lines.push(`Score: ${e.score}`);
70
+ }
71
+ if (isNonEmptyArray(e.courses)) {
72
+ lines.push('', ...e.courses.map(c => `- ${c}`));
73
+ }
74
+ return lines;
75
+ });
76
+ const awards = resume => sec('Awards', resume.awards, a => {
77
+ const lines = [`### ${a.title || 'Award'}`];
78
+ lines.push(...italic([a.awarder, a.date].filter(Boolean).join(' — ')));
79
+ if (a.summary) {
80
+ lines.push('', a.summary);
81
+ }
82
+ return lines;
83
+ });
84
+ const publications = resume => sec('Publications', resume.publications, p => {
85
+ const lines = [`### ${link(p.name || 'Publication', p.url)}`];
86
+ lines.push(...italic([p.publisher, p.releaseDate].filter(Boolean).join(' — ')));
87
+ if (p.summary) {
88
+ lines.push('', p.summary);
89
+ }
90
+ return lines;
91
+ });
92
+ const skills = resume => sec('Skills', resume.skills, s => {
93
+ const heading = [s.name, s.level].filter(Boolean).join(' — ');
94
+ return [`### ${heading || 'Skill'}`].concat(csv(s.keywords));
95
+ });
96
+ const languages = resume => sec('Languages', resume.languages, l => [`- ${[l.language, l.fluency].filter(Boolean).join(' — ')}`]);
97
+ const interests = resume => sec('Interests', resume.interests, i => [`### ${i.name || 'Interest'}`].concat(csv(i.keywords)));
98
+ const references = resume => sec('References', resume.references, r => {
99
+ const lines = [`### ${r.name || 'Reference'}`];
100
+ if (r.reference) {
101
+ lines.push('', `> ${r.reference}`);
102
+ }
103
+ return lines;
104
+ });
105
+ const projects = resume => sec('Projects', resume.projects, p => {
106
+ const lines = [`### ${link(p.name || 'Project', p.url)}`];
107
+ lines.push(...italic(dateRange(p.startDate, p.endDate)));
108
+ if (p.description) {
109
+ lines.push('', p.description);
110
+ }
111
+ return lines.concat(highlights(p.highlights)).concat(csv(p.keywords));
112
+ });
113
+ const toMarkdown = (resume = {}) => {
114
+ const lines = [...basics(resume), ...work(resume), ...volunteer(resume), ...education(resume), ...awards(resume), ...publications(resume), ...skills(resume), ...languages(resume), ...interests(resume), ...references(resume), ...projects(resume)];
115
+ return `${lines.join('\n').trim()}\n`;
116
+ };
117
+ module.exports = toMarkdown;
118
+ module.exports.toMarkdown = toMarkdown;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+
3
+ var _markdown = _interopRequireDefault(require("./markdown"));
4
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
5
+ const sample = require('@jsonresume/schema/sample.resume.json');
6
+ describe('markdown formatter', () => {
7
+ describe('against the bundled sample resume', () => {
8
+ let md;
9
+ beforeAll(() => {
10
+ md = (0, _markdown.default)(sample);
11
+ });
12
+ it('returns a non-empty string ending in a newline', () => {
13
+ expect(typeof md).toBe('string');
14
+ expect(md.length).toBeGreaterThan(0);
15
+ expect(md.endsWith('\n')).toBe(true);
16
+ });
17
+ it('renders the person name as the top-level heading', () => {
18
+ expect(md).toContain('# Richard Hendriks');
19
+ });
20
+ it('renders a heading for every populated section', () => {
21
+ ['## Work', '## Volunteer', '## Education', '## Awards', '## Publications', '## Skills', '## Languages', '## Interests', '## References', '## Projects'].forEach(heading => {
22
+ expect(md).toContain(heading);
23
+ });
24
+ });
25
+ it('renders entry details, highlights and keywords', () => {
26
+ expect(md).toContain('Pied Piper');
27
+ expect(md).toContain('CEO/President');
28
+ expect(md).toContain('- Successfully won Techcrunch Disrupt');
29
+ expect(md).toContain('University of Oklahoma');
30
+ expect(md).toContain('Miss Direction');
31
+ expect(md).toContain('HTML, CSS, Javascript');
32
+ expect(md).toContain('English — Native speaker');
33
+ });
34
+ it('links urls using markdown link syntax', () => {
35
+ expect(md).toContain('](http://piedpiper.example.com)');
36
+ });
37
+ });
38
+ describe('with missing / partial sections', () => {
39
+ it('handles an empty resume without throwing', () => {
40
+ expect(() => (0, _markdown.default)({})).not.toThrow();
41
+ expect(() => (0, _markdown.default)()).not.toThrow();
42
+ });
43
+ it('omits sections that are absent', () => {
44
+ const md = (0, _markdown.default)({
45
+ basics: {
46
+ name: 'Jane Doe'
47
+ }
48
+ });
49
+ expect(md).toContain('# Jane Doe');
50
+ expect(md).not.toContain('## Work');
51
+ expect(md).not.toContain('## Skills');
52
+ });
53
+ it('omits sections that are present but empty', () => {
54
+ const md = (0, _markdown.default)({
55
+ basics: {
56
+ name: 'Jane Doe'
57
+ },
58
+ work: []
59
+ });
60
+ expect(md).not.toContain('## Work');
61
+ });
62
+ });
63
+ });
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+
3
+ // Pure plain-text formatter for a JSON Resume. No external theme required.
4
+ // Renders every JSON Resume schema section that is present, and silently
5
+ // skips sections that are missing or empty so partial resumes work too.
6
+
7
+ const {
8
+ isNonEmptyArray,
9
+ dateRange,
10
+ contactParts,
11
+ section
12
+ } = require('./utils');
13
+ const heading = text => [text.toUpperCase(), '='.repeat(text.length)];
14
+ const sec = (title, items, renderItem) => section(heading(title), items, renderItem);
15
+ const bullets = items => isNonEmptyArray(items) ? items.map(i => ` * ${i}`) : [];
16
+ const csv = keywords => isNonEmptyArray(keywords) ? [` ${keywords.join(', ')}`] : [];
17
+ const basics = resume => {
18
+ const b = resume.basics;
19
+ if (!b) {
20
+ return [];
21
+ }
22
+ const lines = [];
23
+ if (b.name) {
24
+ lines.push(b.name);
25
+ }
26
+ if (b.label) {
27
+ lines.push(b.label);
28
+ }
29
+ const contact = contactParts(b);
30
+ if (contact.length) {
31
+ lines.push(contact.join(' | '));
32
+ }
33
+ if (isNonEmptyArray(b.profiles)) {
34
+ b.profiles.forEach(p => {
35
+ const label = [p.network, p.username].filter(Boolean).join(' ');
36
+ const parts = [label, p.url].filter(Boolean);
37
+ if (parts.length) {
38
+ lines.push(parts.join(': '));
39
+ }
40
+ });
41
+ }
42
+ if (b.summary) {
43
+ lines.push('', b.summary);
44
+ }
45
+ return lines;
46
+ };
47
+ const work = resume => sec('Work', resume.work, w => {
48
+ const title = [w.position, w.name || w.company].filter(Boolean).join(' @ ');
49
+ const lines = [title || 'Role'];
50
+ const range = dateRange(w.startDate, w.endDate);
51
+ if (range) {
52
+ lines.push(range);
53
+ }
54
+ if (w.url) {
55
+ lines.push(w.url);
56
+ }
57
+ if (w.summary) {
58
+ lines.push(w.summary);
59
+ }
60
+ return lines.concat(bullets(w.highlights));
61
+ });
62
+ const volunteer = resume => sec('Volunteer', resume.volunteer, v => {
63
+ const title = [v.position, v.organization].filter(Boolean).join(' @ ');
64
+ const lines = [title || 'Volunteer'];
65
+ const range = dateRange(v.startDate, v.endDate);
66
+ if (range) {
67
+ lines.push(range);
68
+ }
69
+ if (v.summary) {
70
+ lines.push(v.summary);
71
+ }
72
+ return lines.concat(bullets(v.highlights));
73
+ });
74
+ const education = resume => sec('Education', resume.education, e => {
75
+ const study = [e.studyType, e.area].filter(Boolean).join(', ');
76
+ const title = [e.institution, study].filter(Boolean).join(' - ');
77
+ const lines = [title || 'Education'];
78
+ const range = dateRange(e.startDate, e.endDate);
79
+ if (range) {
80
+ lines.push(range);
81
+ }
82
+ if (e.score) {
83
+ lines.push(`Score: ${e.score}`);
84
+ }
85
+ return lines.concat(bullets(e.courses));
86
+ });
87
+ const awards = resume => sec('Awards', resume.awards, a => {
88
+ const lines = [a.title || 'Award'];
89
+ const meta = [a.awarder, a.date].filter(Boolean).join(' - ');
90
+ if (meta) {
91
+ lines.push(meta);
92
+ }
93
+ if (a.summary) {
94
+ lines.push(a.summary);
95
+ }
96
+ return lines;
97
+ });
98
+ const publications = resume => sec('Publications', resume.publications, p => {
99
+ const lines = [p.name || 'Publication'];
100
+ const meta = [p.publisher, p.releaseDate].filter(Boolean).join(' - ');
101
+ if (meta) {
102
+ lines.push(meta);
103
+ }
104
+ if (p.url) {
105
+ lines.push(p.url);
106
+ }
107
+ if (p.summary) {
108
+ lines.push(p.summary);
109
+ }
110
+ return lines;
111
+ });
112
+ const skills = resume => sec('Skills', resume.skills, s => {
113
+ const title = [s.name, s.level].filter(Boolean).join(' - ');
114
+ return [title || 'Skill'].concat(csv(s.keywords));
115
+ });
116
+ const languages = resume => sec('Languages', resume.languages, l => [` * ${[l.language, l.fluency].filter(Boolean).join(' - ')}`]);
117
+ const interests = resume => sec('Interests', resume.interests, i => [i.name || 'Interest'].concat(csv(i.keywords)));
118
+ const references = resume => sec('References', resume.references, r => {
119
+ const lines = [r.name || 'Reference'];
120
+ if (r.reference) {
121
+ lines.push(r.reference);
122
+ }
123
+ return lines;
124
+ });
125
+ const projects = resume => sec('Projects', resume.projects, p => {
126
+ const lines = [p.name || 'Project'];
127
+ const range = dateRange(p.startDate, p.endDate);
128
+ if (range) {
129
+ lines.push(range);
130
+ }
131
+ if (p.url) {
132
+ lines.push(p.url);
133
+ }
134
+ if (p.description) {
135
+ lines.push(p.description);
136
+ }
137
+ return lines.concat(bullets(p.highlights)).concat(csv(p.keywords));
138
+ });
139
+ const toText = (resume = {}) => {
140
+ const lines = [...basics(resume), ...work(resume), ...volunteer(resume), ...education(resume), ...awards(resume), ...publications(resume), ...skills(resume), ...languages(resume), ...interests(resume), ...references(resume), ...projects(resume)];
141
+ return `${lines.join('\n').trim()}\n`;
142
+ };
143
+ module.exports = toText;
144
+ module.exports.toText = toText;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+
3
+ var _text = _interopRequireDefault(require("./text"));
4
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
5
+ const sample = require('@jsonresume/schema/sample.resume.json');
6
+ describe('text formatter', () => {
7
+ describe('against the bundled sample resume', () => {
8
+ let txt;
9
+ beforeAll(() => {
10
+ txt = (0, _text.default)(sample);
11
+ });
12
+ it('returns a non-empty string ending in a newline', () => {
13
+ expect(typeof txt).toBe('string');
14
+ expect(txt.length).toBeGreaterThan(0);
15
+ expect(txt.endsWith('\n')).toBe(true);
16
+ });
17
+ it('renders the person name', () => {
18
+ expect(txt).toContain('Richard Hendriks');
19
+ });
20
+ it('renders an underlined heading for every populated section', () => {
21
+ ['WORK', 'VOLUNTEER', 'EDUCATION', 'AWARDS', 'PUBLICATIONS', 'SKILLS', 'LANGUAGES', 'INTERESTS', 'REFERENCES', 'PROJECTS'].forEach(heading => {
22
+ expect(txt).toContain(heading);
23
+ });
24
+ // headings are underlined with '=' rules
25
+ expect(txt).toContain('====');
26
+ });
27
+ it('renders entry details, highlights and keywords', () => {
28
+ expect(txt).toContain('CEO/President @ Pied Piper');
29
+ expect(txt).toContain(' * Successfully won Techcrunch Disrupt');
30
+ expect(txt).toContain('University of Oklahoma');
31
+ expect(txt).toContain('Miss Direction');
32
+ expect(txt).toContain('HTML, CSS, Javascript');
33
+ expect(txt).toContain('English - Native speaker');
34
+ });
35
+ it('does not contain markdown markup', () => {
36
+ expect(txt).not.toContain('# ');
37
+ expect(txt).not.toContain('](');
38
+ });
39
+ });
40
+ describe('with missing / partial sections', () => {
41
+ it('handles an empty resume without throwing', () => {
42
+ expect(() => (0, _text.default)({})).not.toThrow();
43
+ expect(() => (0, _text.default)()).not.toThrow();
44
+ });
45
+ it('omits sections that are absent', () => {
46
+ const txt = (0, _text.default)({
47
+ basics: {
48
+ name: 'Jane Doe'
49
+ }
50
+ });
51
+ expect(txt).toContain('Jane Doe');
52
+ expect(txt).not.toContain('WORK');
53
+ expect(txt).not.toContain('SKILLS');
54
+ });
55
+ it('omits sections that are present but empty', () => {
56
+ const txt = (0, _text.default)({
57
+ basics: {
58
+ name: 'Jane Doe'
59
+ },
60
+ work: []
61
+ });
62
+ expect(txt).not.toContain('WORK');
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+
3
+ // Shared helpers for the theme-less resume formatters (markdown + text).
4
+
5
+ const isNonEmptyArray = value => Array.isArray(value) && value.length > 0;
6
+ const dateRange = (startDate, endDate) => {
7
+ if (!startDate && !endDate) {
8
+ return '';
9
+ }
10
+ return `${startDate || ''} - ${endDate || 'Present'}`;
11
+ };
12
+
13
+ // Flatten a contact line out of basics: email, phone, url and a one-line
14
+ // location. Returns an array of fragments (caller decides the separator).
15
+ const contactParts = (basics = {}) => {
16
+ const parts = [];
17
+ if (basics.email) {
18
+ parts.push(basics.email);
19
+ }
20
+ if (basics.phone) {
21
+ parts.push(basics.phone);
22
+ }
23
+ if (basics.url) {
24
+ parts.push(basics.url);
25
+ }
26
+ const loc = basics.location;
27
+ if (loc) {
28
+ const locParts = [loc.address, loc.city, loc.region, loc.postalCode, loc.countryCode].filter(Boolean);
29
+ if (locParts.length) {
30
+ parts.push(locParts.join(', '));
31
+ }
32
+ }
33
+ return parts;
34
+ };
35
+
36
+ // Build a section block: returns [] when there are no items, otherwise the
37
+ // `headerLines` followed by each rendered item (separated by a blank line).
38
+ const section = (headerLines, items, renderItem) => {
39
+ if (!isNonEmptyArray(items)) {
40
+ return [];
41
+ }
42
+ const lines = ['', ...headerLines];
43
+ items.forEach(item => {
44
+ lines.push('', ...renderItem(item));
45
+ });
46
+ return lines;
47
+ };
48
+ module.exports = {
49
+ isNonEmptyArray,
50
+ dateRange,
51
+ contactParts,
52
+ section
53
+ };
package/build/main.js CHANGED
@@ -50,7 +50,7 @@ const normalizeTheme = (value, defaultValue) => {
50
50
  process.exitCode = 1;
51
51
  }
52
52
  });
53
- program.command('export [fileName]').description('Export locally to .html or .pdf. Supply a --format <file format> flag and argument to specify export format. Pick a theme with --theme (https://jsonresume.org/themes/).').action(async fileName => {
53
+ program.command('export [fileName]').description('Export locally to .html, .pdf, .md (markdown) or .txt (text). Supply a --format <file format> flag and argument to specify export format. .md and .txt need no theme; pick a theme for .html/.pdf with --theme (https://jsonresume.org/themes/).').action(async fileName => {
54
54
  const resume = await (0, _getResume.default)({
55
55
  path: program.resume
56
56
  });
@@ -83,10 +83,12 @@ describe('cli configuration', () => {
83
83
  Commands:
84
84
  init Initialize a resume.json file
85
85
  validate Validate your resume's schema
86
- export [fileName] Export locally to .html or .pdf. Supply
87
- a --format <file format> flag and
88
- argument to specify export format. Pick a
89
- theme with --theme
86
+ export [fileName] Export locally to .html, .pdf, .md
87
+ (markdown) or .txt (text). Supply a
88
+ --format <file format> flag and argument
89
+ to specify export format. .md and .txt
90
+ need no theme; pick a theme for
91
+ .html/.pdf with --theme
90
92
  (https://jsonresume.org/themes/).
91
93
  serve Serve resume at http://localhost:4000/
92
94
  help [command] display help for command
@@ -170,6 +172,41 @@ describe('cli configuration', () => {
170
172
  "
171
173
  `);
172
174
  });
175
+ it('should export markdown without requiring a theme', async () => {
176
+ const {
177
+ stdout,
178
+ volume
179
+ } = await run(['export', '/test-resumes/exported-resume.md', '--resume', '-', '--format', 'markdown'], {
180
+ stdin: JSON.stringify({
181
+ basics: {
182
+ name: 'thomas-md'
183
+ },
184
+ skills: [{
185
+ name: 'JS',
186
+ keywords: ['Node']
187
+ }]
188
+ })
189
+ });
190
+ const output = volume['/test-resumes/exported-resume.md'];
191
+ expect(output).toEqual(expect.stringContaining('# thomas-md'));
192
+ expect(output).toEqual(expect.stringContaining('## Skills'));
193
+ expect(stdout).toContain('.md resume at:');
194
+ });
195
+ it('should export plain text without requiring a theme', async () => {
196
+ const {
197
+ stdout,
198
+ volume
199
+ } = await run(['export', '/test-resumes/exported-resume.txt', '--resume', '-', '--format', 'text'], {
200
+ stdin: JSON.stringify({
201
+ basics: {
202
+ name: 'thomas-txt'
203
+ }
204
+ })
205
+ });
206
+ const output = volume['/test-resumes/exported-resume.txt'];
207
+ expect(output).toEqual(expect.stringContaining('thomas-txt'));
208
+ expect(stdout).toContain('.txt resume at:');
209
+ });
173
210
  it('should print a friendly, actionable message and exit non-zero when the theme is not installed', async () => {
174
211
  const {
175
212
  code,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resume-cli",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "The JSON Resume command line interface",
5
5
  "main": "build/main.js",
6
6
  "engines": {