jsonresume-theme-engineering 0.2.0 → 0.3.1
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/Makefile +5 -1
- package/index.js +45 -13
- package/package.json +11 -6
- package/reference.pdf +0 -0
- package/renovate.json +10 -0
- package/style.css +11 -3
- package/test/01-render.test.js +105 -0
- package/test/02-pdfExport.test.js +73 -0
- package/test/03-content.test.js +73 -0
- package/test/04-structure.test.js +139 -0
- package/test/06-accessibility.test.js +124 -0
- package/test/utils/pdf-helper.js +73 -0
- package/views/awards.hbs +1 -1
- package/views/basics.hbs +1 -1
- package/views/education.hbs +1 -1
- package/views/volunteer.hbs +2 -2
- package/views/work.hbs +1 -1
- package/.github/dependabot.yml +0 -11
- package/resume.2024-04-2110:16:53.png +0 -0
- package/resume.2024-04-2110:19:18.png +0 -0
- package/resume.2024-04-2111:22:03.png +0 -0
- package/resume.old.png +0 -0
- package/resume.png +0 -0
- package/sample-resume.json +0 -156
package/Makefile
CHANGED
|
@@ -2,7 +2,7 @@ HBS_TEMPLATES = $(sort $(wildcard partials/** views/** resume.hbs ))
|
|
|
2
2
|
|
|
3
3
|
all: resume.png
|
|
4
4
|
|
|
5
|
-
resume.json:
|
|
5
|
+
resume.json: sample-resume.json
|
|
6
6
|
cp sample-resume.json resume.json
|
|
7
7
|
|
|
8
8
|
resume.pdf: resume.json index.js $(HBS_TEMPLATES)
|
|
@@ -10,3 +10,7 @@ resume.pdf: resume.json index.js $(HBS_TEMPLATES)
|
|
|
10
10
|
|
|
11
11
|
resume.png: resume.pdf
|
|
12
12
|
pdftoppm -png resume.pdf > resume.png
|
|
13
|
+
|
|
14
|
+
.PHONY: test
|
|
15
|
+
test:
|
|
16
|
+
npm run test
|
package/index.js
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
const
|
|
2
2
|
fs = require('fs'),
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
path = require('path'),
|
|
4
|
+
Handlebars = require('handlebars'),
|
|
5
5
|
addressFormat = require('address-format'),
|
|
6
|
-
moment = require('moment')
|
|
7
|
-
Swag = require('swag');
|
|
6
|
+
moment = require('moment');
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
handlebars.registerHelper({
|
|
8
|
+
Handlebars.registerHelper({
|
|
12
9
|
|
|
13
10
|
wrapURL: function (url) {
|
|
14
11
|
const wrappedUrl = '<a href="' + url + '">' + url.replace(/.*?:\/\//g, '') + "</a>";
|
|
15
|
-
return new
|
|
12
|
+
return new Handlebars.SafeString(wrappedUrl);
|
|
16
13
|
},
|
|
17
14
|
|
|
18
15
|
wrapMail: function (address) {
|
|
19
16
|
const wrappedAddress = '<a href="mailto:' + address + '">' + address + "</a>";
|
|
20
|
-
return new
|
|
17
|
+
return new Handlebars.SafeString(wrappedAddress);
|
|
21
18
|
},
|
|
22
19
|
|
|
23
20
|
formatAddress: function (address, city, region, postalCode, countryCode) {
|
|
@@ -42,14 +39,49 @@ handlebars.registerHelper({
|
|
|
42
39
|
});
|
|
43
40
|
|
|
44
41
|
function render(resume) {
|
|
42
|
+
if (!resume || typeof resume !== 'object') {
|
|
43
|
+
throw new Error('Expected input to be a valid resume object');
|
|
44
|
+
}
|
|
45
|
+
|
|
45
46
|
let dir = __dirname,
|
|
46
47
|
css = fs.readFileSync(dir + '/style.css', 'utf-8'),
|
|
47
|
-
resumeTemplate = fs.readFileSync(dir + '/resume.hbs', 'utf-8')
|
|
48
|
+
resumeTemplate = fs.readFileSync(dir + '/resume.hbs', 'utf-8'),
|
|
49
|
+
partialsDir = path.join(dir, 'partials'),
|
|
50
|
+
viewsDir = path.join(dir, 'views');
|
|
51
|
+
|
|
52
|
+
// Load partials from partialsDir
|
|
53
|
+
let partialFilenames = fs.readdirSync(partialsDir);
|
|
54
|
+
partialFilenames.forEach(function (filename) {
|
|
55
|
+
var matches = /^([^.]+).hbs$/.exec(filename);
|
|
56
|
+
if (!matches) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
var name = matches[1];
|
|
60
|
+
var filepath = path.join(partialsDir, filename);
|
|
61
|
+
var template = fs.readFileSync(filepath, 'utf8');
|
|
62
|
+
|
|
63
|
+
Handlebars.registerPartial(name, template);
|
|
64
|
+
});
|
|
48
65
|
|
|
49
|
-
|
|
66
|
+
// Load partials from viewsDir (if it exists)
|
|
67
|
+
try {
|
|
68
|
+
if (fs.existsSync(viewsDir)) {
|
|
69
|
+
let viewFilenames = fs.readdirSync(viewsDir);
|
|
70
|
+
viewFilenames.forEach(function (filename) {
|
|
71
|
+
var matches = /^([^.]+).hbs$/.exec(filename);
|
|
72
|
+
if (!matches) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
var name = matches[1];
|
|
76
|
+
var filepath = path.join(viewsDir, filename);
|
|
77
|
+
var template = fs.readFileSync(filepath, 'utf8');
|
|
50
78
|
|
|
51
|
-
|
|
52
|
-
|
|
79
|
+
Handlebars.registerPartial(name, template);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error('Error loading views directory:', err);
|
|
84
|
+
}
|
|
53
85
|
|
|
54
86
|
return Handlebars.compile(resumeTemplate)({
|
|
55
87
|
css: css,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsonresume-theme-engineering",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "JSON Resume theme for engineers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jsonresume",
|
|
@@ -18,16 +18,21 @@
|
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"scripts": {
|
|
20
20
|
"start": "resume serve --theme .",
|
|
21
|
-
"export": "resume export --theme . resume.pdf"
|
|
21
|
+
"export": "resume export --theme . resume.pdf",
|
|
22
|
+
"test": "mocha test/*.test.js"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"address-format": "0.0.3",
|
|
25
26
|
"handlebars": "^4.7.8",
|
|
26
|
-
"
|
|
27
|
-
"moment": "^2.30.1",
|
|
28
|
-
"swag": "^0.7.0"
|
|
27
|
+
"moment": "^2.30.1"
|
|
29
28
|
},
|
|
30
29
|
"devDependencies": {
|
|
31
|
-
"
|
|
30
|
+
"looks-same": "^9.0.1",
|
|
31
|
+
"mocha": "^10.0.0",
|
|
32
|
+
"pdf-img-convert": "^2.0.0",
|
|
33
|
+
"pdf-parse": "^1.1.1",
|
|
34
|
+
"pngjs": "^7.0.0",
|
|
35
|
+
"resume-cli": "^3.1.2",
|
|
36
|
+
"sinon": "^21.0.0"
|
|
32
37
|
}
|
|
33
38
|
}
|
package/reference.pdf
ADDED
|
Binary file
|
package/renovate.json
ADDED
package/style.css
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
body {
|
|
3
3
|
font-family: Georgia, serif;
|
|
4
4
|
font-size: 11px;
|
|
5
|
-
|
|
5
|
+
display: flex;
|
|
6
|
+
justify-content: center;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
#resume {
|
|
10
|
+
max-width: 800px;
|
|
11
|
+
width: 100%;
|
|
12
|
+
padding: 24px 48px;
|
|
13
|
+
box-sizing: border-box;
|
|
6
14
|
}
|
|
7
15
|
|
|
8
16
|
h1 {
|
|
@@ -83,7 +91,7 @@ a {
|
|
|
83
91
|
size: portrait;
|
|
84
92
|
margin: 10mm 25mm;
|
|
85
93
|
}
|
|
86
|
-
|
|
94
|
+
#resume {
|
|
87
95
|
max-width: 100%;
|
|
88
96
|
border: 0px;
|
|
89
97
|
background: #fff;
|
|
@@ -92,7 +100,7 @@ a {
|
|
|
92
100
|
}
|
|
93
101
|
body,
|
|
94
102
|
html,
|
|
95
|
-
|
|
103
|
+
#resume {
|
|
96
104
|
margin: 0px;
|
|
97
105
|
padding: 0px;
|
|
98
106
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { render } = require('../index');
|
|
5
|
+
|
|
6
|
+
describe('Render', function() {
|
|
7
|
+
it('should render a resume with valid input', function() {
|
|
8
|
+
const resume = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'sample-resume.json'), 'utf-8'));
|
|
9
|
+
const output = render(resume);
|
|
10
|
+
assert(output.includes('<title>Richard Hendriks</title>'), 'Output should include the name from the resume');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should throw an error with invalid input', function() {
|
|
14
|
+
assert.throws(() => render(null), Error, 'Expected input to be a valid resume object');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should handle an empty resume object', function() {
|
|
18
|
+
const emptyResume = {};
|
|
19
|
+
const output = render(emptyResume);
|
|
20
|
+
assert(output.includes('<title></title>'), 'Output should handle an empty resume object');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should handle a resume object missing required fields', function() {
|
|
24
|
+
const incompleteResume = {
|
|
25
|
+
basics: {
|
|
26
|
+
name: 'Jane Doe'
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const output = render(incompleteResume);
|
|
30
|
+
assert(output.includes('<title>Jane Doe</title>'), 'Output should handle a resume object missing required fields');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should throw an error with invalid data type', function() {
|
|
34
|
+
assert.throws(() => render('invalid input'), Error, 'Expected input to be a valid resume object');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should handle a large resume object', function() {
|
|
38
|
+
const largeResume = {
|
|
39
|
+
basics: {
|
|
40
|
+
name: 'Large Resume',
|
|
41
|
+
label: 'Test',
|
|
42
|
+
email: 'large.resume@example.com',
|
|
43
|
+
phone: '(123) 456-7890',
|
|
44
|
+
website: 'http://example.com',
|
|
45
|
+
summary: 'This is a large resume object.',
|
|
46
|
+
location: {
|
|
47
|
+
address: '123 Main St',
|
|
48
|
+
postalCode: '12345',
|
|
49
|
+
city: 'Anytown',
|
|
50
|
+
countryCode: 'US',
|
|
51
|
+
region: 'CA'
|
|
52
|
+
},
|
|
53
|
+
profiles: []
|
|
54
|
+
},
|
|
55
|
+
work: Array(1000).fill({
|
|
56
|
+
company: 'Large Company',
|
|
57
|
+
position: 'Software Engineer',
|
|
58
|
+
website: 'http://example.com',
|
|
59
|
+
startDate: '2000-01-01',
|
|
60
|
+
summary: 'Worked on various projects.',
|
|
61
|
+
highlights: ['Highlight 1', 'Highlight 2']
|
|
62
|
+
}),
|
|
63
|
+
education: [],
|
|
64
|
+
skills: [],
|
|
65
|
+
awards: [],
|
|
66
|
+
publications: [],
|
|
67
|
+
languages: [],
|
|
68
|
+
interests: [],
|
|
69
|
+
references: []
|
|
70
|
+
};
|
|
71
|
+
const output = render(largeResume);
|
|
72
|
+
assert(output.includes('<title>Large Resume</title>'), 'Output should handle a large resume object');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle special characters in fields', function() {
|
|
76
|
+
const specialCharsResume = {
|
|
77
|
+
basics: {
|
|
78
|
+
name: 'Special & Ch@rs',
|
|
79
|
+
label: 'Test',
|
|
80
|
+
email: 'special.chars@example.com',
|
|
81
|
+
phone: '(123) 456-7890',
|
|
82
|
+
website: 'http://example.com',
|
|
83
|
+
summary: 'This is a summary with special characters: &, <, >, ", \'.',
|
|
84
|
+
location: {
|
|
85
|
+
address: '123 Main St',
|
|
86
|
+
postalCode: '12345',
|
|
87
|
+
city: 'Anytown',
|
|
88
|
+
countryCode: 'US',
|
|
89
|
+
region: 'CA'
|
|
90
|
+
},
|
|
91
|
+
profiles: []
|
|
92
|
+
},
|
|
93
|
+
work: [],
|
|
94
|
+
education: [],
|
|
95
|
+
skills: [],
|
|
96
|
+
awards: [],
|
|
97
|
+
publications: [],
|
|
98
|
+
languages: [],
|
|
99
|
+
interests: [],
|
|
100
|
+
references: []
|
|
101
|
+
};
|
|
102
|
+
const output = render(specialCharsResume);
|
|
103
|
+
assert(output.includes('<title>Special & Ch@rs</title>'), 'Output should handle special characters in fields');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
const childProcess = require('child_process');
|
|
6
|
+
const pdfHelper = require('./utils/pdf-helper');
|
|
7
|
+
|
|
8
|
+
describe('PDF Export', function() {
|
|
9
|
+
let execStub;
|
|
10
|
+
|
|
11
|
+
beforeEach(function() {
|
|
12
|
+
// Backup the original PDF
|
|
13
|
+
pdfHelper.backupReferencePdf();
|
|
14
|
+
|
|
15
|
+
// Create a proper sinon stub for the exec function
|
|
16
|
+
execStub = sinon.stub(childProcess, 'exec');
|
|
17
|
+
|
|
18
|
+
// Configure the stub to simulate successful PDF generation
|
|
19
|
+
execStub.callsFake((command, callback) => {
|
|
20
|
+
if (command === 'npm run export') {
|
|
21
|
+
// Simulate the PDF export process
|
|
22
|
+
fs.writeFileSync(pdfHelper.referencePdfPath, 'PDF content');
|
|
23
|
+
callback(null, 'PDF export successful');
|
|
24
|
+
} else {
|
|
25
|
+
callback(new Error('Unknown command'));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(function() {
|
|
31
|
+
// Restore all sinon stubs
|
|
32
|
+
sinon.restore();
|
|
33
|
+
|
|
34
|
+
// Restore the original PDF
|
|
35
|
+
pdfHelper.restoreReferencePdf();
|
|
36
|
+
|
|
37
|
+
// Clean up test PDF
|
|
38
|
+
pdfHelper.cleanupTestPdf();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should export a PDF with valid input', function(done) {
|
|
42
|
+
const resume = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'sample-resume.json'), 'utf-8'));
|
|
43
|
+
fs.writeFileSync(path.join(__dirname, '..', 'resume.json'), JSON.stringify(resume));
|
|
44
|
+
|
|
45
|
+
childProcess.exec('npm run export', (error, stdout, stderr) => {
|
|
46
|
+
if (error) {
|
|
47
|
+
return done(error);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
assert(fs.existsSync(pdfHelper.referencePdfPath), 'PDF file should be generated');
|
|
51
|
+
|
|
52
|
+
// Read the file as a Buffer instead of utf-8 string since PDF is binary
|
|
53
|
+
const pdfExists = fs.existsSync(pdfHelper.referencePdfPath);
|
|
54
|
+
assert(pdfExists, 'PDF file should be generated');
|
|
55
|
+
|
|
56
|
+
done();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should handle errors during PDF export', function(done) {
|
|
61
|
+
// Restore the original stub and create a new one that simulates an error
|
|
62
|
+
sinon.restore();
|
|
63
|
+
execStub = sinon.stub(childProcess, 'exec');
|
|
64
|
+
execStub.callsFake((command, callback) => {
|
|
65
|
+
callback(new Error('Simulated error during PDF export'));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
childProcess.exec('npm run export', (error, stdout, stderr) => {
|
|
69
|
+
assert(error, 'An error should be thrown during PDF export');
|
|
70
|
+
done();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
const childProcess = require('child_process');
|
|
6
|
+
const pdfHelper = require('./utils/pdf-helper');
|
|
7
|
+
|
|
8
|
+
describe('PDF Export', function() {
|
|
9
|
+
let execStub;
|
|
10
|
+
|
|
11
|
+
beforeEach(function() {
|
|
12
|
+
// Backup the original PDF
|
|
13
|
+
pdfHelper.backupReferencePdf();
|
|
14
|
+
|
|
15
|
+
// Create a proper sinon stub for the exec function
|
|
16
|
+
execStub = sinon.stub(childProcess, 'exec');
|
|
17
|
+
|
|
18
|
+
// Configure the stub to simulate successful PDF generation
|
|
19
|
+
execStub.callsFake((command, callback) => {
|
|
20
|
+
if (command === 'npm run export') {
|
|
21
|
+
// Simulate the PDF export process
|
|
22
|
+
fs.writeFileSync(pdfHelper.referencePdfPath, 'PDF content');
|
|
23
|
+
callback(null, 'PDF export successful');
|
|
24
|
+
} else {
|
|
25
|
+
callback(new Error('Unknown command'));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(function() {
|
|
31
|
+
// Restore all sinon stubs
|
|
32
|
+
sinon.restore();
|
|
33
|
+
|
|
34
|
+
// Restore the original PDF
|
|
35
|
+
pdfHelper.restoreReferencePdf();
|
|
36
|
+
|
|
37
|
+
// Clean up test PDF
|
|
38
|
+
pdfHelper.cleanupTestPdf();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should export a PDF with valid input', function(done) {
|
|
42
|
+
const resume = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'sample-resume.json'), 'utf-8'));
|
|
43
|
+
fs.writeFileSync(path.join(__dirname, '..', 'resume.json'), JSON.stringify(resume));
|
|
44
|
+
|
|
45
|
+
childProcess.exec('npm run export', (error, stdout, stderr) => {
|
|
46
|
+
if (error) {
|
|
47
|
+
return done(error);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
assert(fs.existsSync(pdfHelper.referencePdfPath), 'PDF file should be generated');
|
|
51
|
+
|
|
52
|
+
// Read the file as a Buffer instead of utf-8 string since PDF is binary
|
|
53
|
+
const pdfExists = fs.existsSync(pdfHelper.referencePdfPath);
|
|
54
|
+
assert(pdfExists, 'PDF file should be generated');
|
|
55
|
+
|
|
56
|
+
done();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should handle errors during PDF export', function(done) {
|
|
61
|
+
// Restore the original stub and create a new one that simulates an error
|
|
62
|
+
sinon.restore();
|
|
63
|
+
execStub = sinon.stub(childProcess, 'exec');
|
|
64
|
+
execStub.callsFake((command, callback) => {
|
|
65
|
+
callback(new Error('Simulated error during PDF export'));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
childProcess.exec('npm run export', (error, stdout, stderr) => {
|
|
69
|
+
assert(error, 'An error should be thrown during PDF export');
|
|
70
|
+
done();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { exec } = require('child_process');
|
|
5
|
+
const pdfParse = require('pdf-parse');
|
|
6
|
+
const pdfHelper = require('./utils/pdf-helper');
|
|
7
|
+
|
|
8
|
+
describe('Resume Structure and Metadata', function() {
|
|
9
|
+
this.timeout(10000);
|
|
10
|
+
|
|
11
|
+
before(function(done) {
|
|
12
|
+
// Backup the original PDF
|
|
13
|
+
pdfHelper.backupReferencePdf();
|
|
14
|
+
|
|
15
|
+
// Generate a test PDF
|
|
16
|
+
pdfHelper.generateTestPdf(done);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
after(function() {
|
|
20
|
+
// Restore the original PDF
|
|
21
|
+
pdfHelper.restoreReferencePdf();
|
|
22
|
+
|
|
23
|
+
// Clean up test PDF
|
|
24
|
+
pdfHelper.cleanupTestPdf();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should maintain proper formatting', function(done) {
|
|
28
|
+
// Get the appropriate PDF path for testing
|
|
29
|
+
const pdfToTest = pdfHelper.getPdfPathForTesting();
|
|
30
|
+
|
|
31
|
+
// Read and parse the PDF
|
|
32
|
+
const dataBuffer = fs.readFileSync(pdfToTest);
|
|
33
|
+
pdfParse(dataBuffer).then(data => {
|
|
34
|
+
const text = data.text;
|
|
35
|
+
|
|
36
|
+
// Check for formatting patterns - more flexible patterns
|
|
37
|
+
const datePatterns = [
|
|
38
|
+
/\d{4}\s*-\s*\d{4}/i, // 2014-2016
|
|
39
|
+
/\d{4}\s*-\s*Present/i, // 2014-Present
|
|
40
|
+
/\d{1,2}\/\d{4}\s*-\s*\d{1,2}\/\d{4}/i, // 05/2014-06/2016
|
|
41
|
+
/\d{1,2}\/\d{4}\s*-\s*Present/i, // 05/2014-Present
|
|
42
|
+
/[A-Z][a-z]{2}\s+\d{4}/ // May 2014
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const hasDatePattern = datePatterns.some(pattern => text.match(pattern));
|
|
46
|
+
assert(hasDatePattern, 'Some form of date formatting should be present');
|
|
47
|
+
|
|
48
|
+
// Check for email pattern - more flexible
|
|
49
|
+
const emailPattern = /\S+@\S+\.\S+/;
|
|
50
|
+
assert(text.match(emailPattern), 'Email should be properly formatted');
|
|
51
|
+
|
|
52
|
+
done();
|
|
53
|
+
}).catch(done);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should have consistent PDF metadata', function(done) {
|
|
57
|
+
// Get the appropriate PDF path for testing
|
|
58
|
+
const pdfToTest = pdfHelper.getPdfPathForTesting();
|
|
59
|
+
|
|
60
|
+
// Read and parse the PDF
|
|
61
|
+
const dataBuffer = fs.readFileSync(pdfToTest);
|
|
62
|
+
pdfParse(dataBuffer).then(data => {
|
|
63
|
+
// Check PDF metadata
|
|
64
|
+
assert(data.info, 'PDF should have metadata');
|
|
65
|
+
assert(data.numpages >= 1, 'PDF should have at least 1 page');
|
|
66
|
+
|
|
67
|
+
// Check PDF size is reasonable (not too small, not too large)
|
|
68
|
+
const pdfSizeKB = dataBuffer.length / 1024;
|
|
69
|
+
assert(pdfSizeKB > 5, 'PDF should not be too small (< 5KB)');
|
|
70
|
+
assert(pdfSizeKB < 2000, 'PDF should not be too large (> 2000KB)');
|
|
71
|
+
|
|
72
|
+
done();
|
|
73
|
+
}).catch(done);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should have consistent file size with reference', function() {
|
|
77
|
+
// Get the appropriate PDF path for testing
|
|
78
|
+
const pdfToTest = pdfHelper.getPdfPathForTesting();
|
|
79
|
+
|
|
80
|
+
// Get the size of the generated PDF
|
|
81
|
+
const pdfSize = fs.statSync(pdfToTest).size;
|
|
82
|
+
|
|
83
|
+
// Get the size of the reference PDF if it exists
|
|
84
|
+
let originalReferencePdfPath = path.join(__dirname, '..', 'reference.pdf');
|
|
85
|
+
if (!fs.existsSync(originalReferencePdfPath)) {
|
|
86
|
+
// If reference.pdf doesn't exist, create it from the current PDF
|
|
87
|
+
fs.copyFileSync(pdfToTest, originalReferencePdfPath);
|
|
88
|
+
this.skip(); // Skip this test for now
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const referencePdfSize = fs.statSync(originalReferencePdfPath).size;
|
|
93
|
+
|
|
94
|
+
// Allow for some variation in file size (±20%)
|
|
95
|
+
const sizeDifferencePercent = Math.abs(pdfSize - referencePdfSize) / referencePdfSize * 100;
|
|
96
|
+
assert(sizeDifferencePercent < 20, `PDF file size differs by ${sizeDifferencePercent.toFixed(2)}% from reference`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should have sections in a logical order', function(done) {
|
|
100
|
+
// Get the appropriate PDF path for testing
|
|
101
|
+
const pdfToTest = pdfHelper.getPdfPathForTesting();
|
|
102
|
+
|
|
103
|
+
// Read and parse the PDF
|
|
104
|
+
const dataBuffer = fs.readFileSync(pdfToTest);
|
|
105
|
+
pdfParse(dataBuffer).then(data => {
|
|
106
|
+
const text = data.text;
|
|
107
|
+
const resume = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'sample-resume.json'), 'utf-8'));
|
|
108
|
+
|
|
109
|
+
// Get the position of the name in the text
|
|
110
|
+
const namePos = text.indexOf(resume.basics.name);
|
|
111
|
+
assert(namePos !== -1, 'Name should be present');
|
|
112
|
+
|
|
113
|
+
// Define possible section headers
|
|
114
|
+
const sectionHeaders = [
|
|
115
|
+
'Work Experience', 'Experience', 'Employment',
|
|
116
|
+
'Education', 'Skills', 'Projects'
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
// Find positions of each section header in the text
|
|
120
|
+
const positions = {};
|
|
121
|
+
sectionHeaders.forEach(header => {
|
|
122
|
+
const pos = text.indexOf(header);
|
|
123
|
+
if (pos !== -1) {
|
|
124
|
+
positions[header] = pos;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Check that we found at least some sections
|
|
129
|
+
assert(Object.keys(positions).length >= 1, 'At least 1 section header should be present');
|
|
130
|
+
|
|
131
|
+
// Check that name appears before any section
|
|
132
|
+
for (const [header, pos] of Object.entries(positions)) {
|
|
133
|
+
assert(namePos < pos, `Name should appear before the "${header}" section`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
done();
|
|
137
|
+
}).catch(done);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const pdfParse = require('pdf-parse');
|
|
5
|
+
const pdfHelper = require('./utils/pdf-helper');
|
|
6
|
+
|
|
7
|
+
describe('Accessibility and Usability', function() {
|
|
8
|
+
this.timeout(10000);
|
|
9
|
+
|
|
10
|
+
before(function(done) {
|
|
11
|
+
// Backup the original PDF
|
|
12
|
+
pdfHelper.backupReferencePdf();
|
|
13
|
+
|
|
14
|
+
// Generate a test PDF
|
|
15
|
+
pdfHelper.generateTestPdf(done);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
after(function() {
|
|
19
|
+
// Restore the original PDF
|
|
20
|
+
pdfHelper.restoreReferencePdf();
|
|
21
|
+
|
|
22
|
+
// Clean up test PDF
|
|
23
|
+
pdfHelper.cleanupTestPdf();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should have a reasonable file size', function() {
|
|
27
|
+
// Get the appropriate PDF path for testing
|
|
28
|
+
const pdfToTest = pdfHelper.getPdfPathForTesting();
|
|
29
|
+
|
|
30
|
+
const stats = fs.statSync(pdfToTest);
|
|
31
|
+
const fileSizeInKB = stats.size / 1024;
|
|
32
|
+
|
|
33
|
+
console.log(`PDF file size: ${fileSizeInKB.toFixed(2)} KB`);
|
|
34
|
+
|
|
35
|
+
// Check that the file size is reasonable (not too small, not too large)
|
|
36
|
+
assert(fileSizeInKB > 10, 'PDF should not be too small (< 10KB)');
|
|
37
|
+
assert(fileSizeInKB < 1000, 'PDF should not be too large (> 1000KB)');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should have extractable text', function(done) {
|
|
41
|
+
// Get the appropriate PDF path for testing
|
|
42
|
+
const pdfToTest = pdfHelper.getPdfPathForTesting();
|
|
43
|
+
|
|
44
|
+
// Read and parse the PDF
|
|
45
|
+
const dataBuffer = fs.readFileSync(pdfToTest);
|
|
46
|
+
pdfParse(dataBuffer).then(data => {
|
|
47
|
+
const text = data.text;
|
|
48
|
+
|
|
49
|
+
// Check that the PDF has extractable text (important for accessibility)
|
|
50
|
+
assert(text.length > 100, 'PDF should have extractable text');
|
|
51
|
+
|
|
52
|
+
// Check that the text contains meaningful content
|
|
53
|
+
assert(text.split(/\s+/).length > 50, 'PDF should contain a reasonable amount of text');
|
|
54
|
+
|
|
55
|
+
done();
|
|
56
|
+
}).catch(done);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should have appropriate metadata', function(done) {
|
|
60
|
+
// Get the appropriate PDF path for testing
|
|
61
|
+
const pdfToTest = pdfHelper.getPdfPathForTesting();
|
|
62
|
+
|
|
63
|
+
// Read and parse the PDF
|
|
64
|
+
const dataBuffer = fs.readFileSync(pdfToTest);
|
|
65
|
+
pdfParse(dataBuffer).then(data => {
|
|
66
|
+
const info = data.info;
|
|
67
|
+
|
|
68
|
+
// Check that the PDF has basic metadata
|
|
69
|
+
assert(info, 'PDF should have metadata');
|
|
70
|
+
|
|
71
|
+
// Log the metadata for informational purposes
|
|
72
|
+
console.log('PDF metadata:', JSON.stringify(info, null, 2));
|
|
73
|
+
|
|
74
|
+
done();
|
|
75
|
+
}).catch(done);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should have a reasonable page count', function(done) {
|
|
79
|
+
// Get the appropriate PDF path for testing
|
|
80
|
+
const pdfToTest = pdfHelper.getPdfPathForTesting();
|
|
81
|
+
|
|
82
|
+
// Read and parse the PDF
|
|
83
|
+
const dataBuffer = fs.readFileSync(pdfToTest);
|
|
84
|
+
pdfParse(dataBuffer).then(data => {
|
|
85
|
+
const pageCount = data.numpages;
|
|
86
|
+
|
|
87
|
+
console.log(`PDF page count: ${pageCount}`);
|
|
88
|
+
|
|
89
|
+
// Check that the PDF has a reasonable number of pages
|
|
90
|
+
assert(pageCount >= 1, 'PDF should have at least 1 page');
|
|
91
|
+
assert(pageCount <= 3, 'PDF should not have more than 3 pages for a typical resume');
|
|
92
|
+
|
|
93
|
+
done();
|
|
94
|
+
}).catch(done);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should include contact information', function(done) {
|
|
98
|
+
// Get the appropriate PDF path for testing
|
|
99
|
+
const pdfToTest = pdfHelper.getPdfPathForTesting();
|
|
100
|
+
|
|
101
|
+
// Read and parse the PDF
|
|
102
|
+
const dataBuffer = fs.readFileSync(pdfToTest);
|
|
103
|
+
pdfParse(dataBuffer).then(data => {
|
|
104
|
+
const text = data.text;
|
|
105
|
+
const resume = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'sample-resume.json'), 'utf-8'));
|
|
106
|
+
|
|
107
|
+
// Check that contact information is included
|
|
108
|
+
assert(text.includes(resume.basics.email), 'Email should be included');
|
|
109
|
+
|
|
110
|
+
// Phone might be formatted differently
|
|
111
|
+
const phoneDigits = resume.basics.phone.replace(/\D/g, '');
|
|
112
|
+
const hasPhone = text.includes(phoneDigits) ||
|
|
113
|
+
text.includes(phoneDigits.substring(phoneDigits.length - 4));
|
|
114
|
+
assert(hasPhone, 'Phone number should be included');
|
|
115
|
+
|
|
116
|
+
// Website might be formatted differently
|
|
117
|
+
const websiteDomain = resume.basics.website.replace(/https?:\/\//i, '').replace(/\/$/, '');
|
|
118
|
+
const hasWebsite = text.includes(websiteDomain) || text.includes(resume.basics.website);
|
|
119
|
+
assert(hasWebsite, 'Website should be included');
|
|
120
|
+
|
|
121
|
+
done();
|
|
122
|
+
}).catch(done);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { exec } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// Paths
|
|
6
|
+
const referencePdfPath = path.join(__dirname, '..', '..', 'resume.pdf');
|
|
7
|
+
const testPdfPath = path.join(__dirname, '..', '..', 'test-resume.pdf');
|
|
8
|
+
|
|
9
|
+
// Save original PDF if it exists
|
|
10
|
+
let originalPdfExists = false;
|
|
11
|
+
let originalPdfContent = null;
|
|
12
|
+
|
|
13
|
+
function backupReferencePdf() {
|
|
14
|
+
originalPdfExists = fs.existsSync(referencePdfPath);
|
|
15
|
+
if (originalPdfExists) {
|
|
16
|
+
originalPdfContent = fs.readFileSync(referencePdfPath);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function restoreReferencePdf() {
|
|
21
|
+
if (originalPdfExists && originalPdfContent) {
|
|
22
|
+
fs.writeFileSync(referencePdfPath, originalPdfContent);
|
|
23
|
+
} else if (fs.existsSync(referencePdfPath) && !originalPdfExists) {
|
|
24
|
+
fs.unlinkSync(referencePdfPath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cleanupTestPdf() {
|
|
29
|
+
if (fs.existsSync(testPdfPath)) {
|
|
30
|
+
fs.unlinkSync(testPdfPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function generateTestPdf(callback) {
|
|
35
|
+
const resume = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'sample-resume.json'), 'utf-8'));
|
|
36
|
+
fs.writeFileSync(path.join(__dirname, '..', '..', 'resume.json'), JSON.stringify(resume));
|
|
37
|
+
|
|
38
|
+
// Try to generate a test PDF with a different name using the correct command format
|
|
39
|
+
exec(`resume export --theme . ${testPdfPath}`, (error) => {
|
|
40
|
+
if (error) {
|
|
41
|
+
console.warn('Warning: Could not generate test PDF with custom name. Trying standard export.');
|
|
42
|
+
|
|
43
|
+
// Fall back to standard export command from package.json
|
|
44
|
+
exec('npm run export', (stdError) => {
|
|
45
|
+
if (stdError) {
|
|
46
|
+
return callback(new Error('Failed to generate PDF for testing: ' + stdError.message));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!fs.existsSync(referencePdfPath)) {
|
|
50
|
+
return callback(new Error('PDF was not generated at expected path: ' + referencePdfPath));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
callback();
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
callback();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getPdfPathForTesting() {
|
|
62
|
+
return fs.existsSync(testPdfPath) ? testPdfPath : referencePdfPath;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
referencePdfPath,
|
|
67
|
+
testPdfPath,
|
|
68
|
+
backupReferencePdf,
|
|
69
|
+
restoreReferencePdf,
|
|
70
|
+
cleanupTestPdf,
|
|
71
|
+
generateTestPdf,
|
|
72
|
+
getPdfPathForTesting
|
|
73
|
+
};
|
package/views/awards.hbs
CHANGED
package/views/basics.hbs
CHANGED
package/views/education.hbs
CHANGED
package/views/volunteer.hbs
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
{{#> item title=organization subtitle=position location=location startDate=startDate endDate=endDate }}
|
|
6
6
|
{{#if summary}}
|
|
7
7
|
<div class="summary">
|
|
8
|
-
<p>{{summary}}</p>
|
|
8
|
+
<p>{{{summary}}}</p>
|
|
9
9
|
</div>
|
|
10
10
|
{{/if}}
|
|
11
11
|
|
|
12
12
|
{{#if highlights.length}}
|
|
13
13
|
<ul class="highlights">
|
|
14
14
|
{{#each highlights}}
|
|
15
|
-
<li>{{.}}</li>
|
|
15
|
+
<li>{{{.}}}</li>
|
|
16
16
|
{{/each}}
|
|
17
17
|
</ul>
|
|
18
18
|
{{/if}}
|
package/views/work.hbs
CHANGED
package/.github/dependabot.yml
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
-
# package ecosystems to update and where the package manifests are located.
|
|
3
|
-
# Please see the documentation for all configuration options:
|
|
4
|
-
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
-
|
|
6
|
-
version: 2
|
|
7
|
-
updates:
|
|
8
|
-
- package-ecosystem: "npm"
|
|
9
|
-
directory: "/" # Location of package manifests
|
|
10
|
-
schedule:
|
|
11
|
-
interval: "weekly"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/resume.old.png
DELETED
|
Binary file
|
package/resume.png
DELETED
|
Binary file
|
package/sample-resume.json
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"basics": {
|
|
3
|
-
"name": "Richard Hendriks",
|
|
4
|
-
"label": "Programmer",
|
|
5
|
-
"picture": "http://www.piedpiper.com/app/themes/pied-piper/dist/images/richard.png",
|
|
6
|
-
"email": "richard.hendriks@piedpiper.com",
|
|
7
|
-
"phone": "(912) 555-4321",
|
|
8
|
-
"website": "http://piedpiper.com",
|
|
9
|
-
"summary": "Richard hails from Tulsa. He has earned degrees from the University of Oklahoma and Stanford. (Go Sooners and Cardinals!) Before starting Pied Piper, he worked for Hooli as a part time software developer. While his work focuses on applied information theory, mostly optimizing lossless compression schema of both the length-limited and adaptive variants, his non-work interests range widely, everything from quantum computing to chaos theory. He could tell you about it, but THAT would NOT be a “length-limited” conversation!",
|
|
10
|
-
"location": {
|
|
11
|
-
"address": "Newell Road",
|
|
12
|
-
"postalCode": "94303",
|
|
13
|
-
"city": "Palo Alto",
|
|
14
|
-
"countryCode": "US",
|
|
15
|
-
"region": "CA"
|
|
16
|
-
},
|
|
17
|
-
"profiles": [
|
|
18
|
-
{
|
|
19
|
-
"network": "Twitter",
|
|
20
|
-
"username": "siliconHBO",
|
|
21
|
-
"url": "https://twitter.com/siliconHBO"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"network": "Facebook",
|
|
25
|
-
"username": "SiliconHBO",
|
|
26
|
-
"url": "https://www.facebook.com/SiliconHBO"
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"network": "Instagram",
|
|
30
|
-
"username": "siliconhbo",
|
|
31
|
-
"url": "https://www.instagram.com/siliconhbo/"
|
|
32
|
-
}
|
|
33
|
-
]
|
|
34
|
-
},
|
|
35
|
-
"work": [
|
|
36
|
-
{
|
|
37
|
-
"company": "Pied Piper",
|
|
38
|
-
"position": "CEO/President",
|
|
39
|
-
"website": "http://piedpiper.com",
|
|
40
|
-
"startDate": "2014-04-13",
|
|
41
|
-
"summary": "Pied Piper is a multi-platform technology based on a proprietary universal compression algorithm that has consistently fielded high Weisman Scores™ that are not merely competitive, but approach the theoretical limit of lossless compression.",
|
|
42
|
-
"highlights": [
|
|
43
|
-
"Build an algorithm for artist to detect if their music was violating copy right infringement laws",
|
|
44
|
-
"Successfully won Techcrunch Disrupt",
|
|
45
|
-
"Optimized an algorithm that holds the current world record for Weisman Scores"
|
|
46
|
-
]
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"company": "Hooli",
|
|
50
|
-
"position": "Senior Software Engineer",
|
|
51
|
-
"website": "http://www.hooli.xyz/",
|
|
52
|
-
"startDate": "2014-01-01",
|
|
53
|
-
"endDate": "2014-04-06",
|
|
54
|
-
"highlights": [
|
|
55
|
-
"Worked on optimizing the backend algorithms for Hooli"
|
|
56
|
-
]
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
"company": "Hooli",
|
|
60
|
-
"position": "Software Engineer",
|
|
61
|
-
"website": "http://www.hooli.xyz/",
|
|
62
|
-
"startDate": "2013-01-01",
|
|
63
|
-
"endDate": "2014-01-01",
|
|
64
|
-
"highlights": [
|
|
65
|
-
"Contributed bugfixes and smaller features for Hooli"
|
|
66
|
-
]
|
|
67
|
-
}
|
|
68
|
-
],
|
|
69
|
-
"volunteer": [
|
|
70
|
-
{
|
|
71
|
-
"organization": "CoderDojo",
|
|
72
|
-
"position": "Teacher",
|
|
73
|
-
"website": "http://coderdojo.com/",
|
|
74
|
-
"startDate": "2012-01-01",
|
|
75
|
-
"endDate": "2013-01-01",
|
|
76
|
-
"summary": "Global movement of free coding clubs for young people.",
|
|
77
|
-
"highlights": [
|
|
78
|
-
"Awarded 'Teacher of the Month'"
|
|
79
|
-
]
|
|
80
|
-
}
|
|
81
|
-
],
|
|
82
|
-
"education": [
|
|
83
|
-
{
|
|
84
|
-
"institution": "Stanford",
|
|
85
|
-
"area": "Computer Science",
|
|
86
|
-
"studyType": "B.S",
|
|
87
|
-
"location": "Palo Alto, CA",
|
|
88
|
-
"specialization": "Machine Learning",
|
|
89
|
-
"startDate": "2011-06-01",
|
|
90
|
-
"endDate": "2014-01-01",
|
|
91
|
-
"gpa": "GPA 4.0",
|
|
92
|
-
"courses": [
|
|
93
|
-
"DB1101 - Basic SQL",
|
|
94
|
-
"CS2011 - Java Introduction"
|
|
95
|
-
]
|
|
96
|
-
}
|
|
97
|
-
],
|
|
98
|
-
"awards": [
|
|
99
|
-
{
|
|
100
|
-
"title": "Digital Compression Pioneer Award",
|
|
101
|
-
"date": "2014-11-01",
|
|
102
|
-
"awarder": "Techcrunch",
|
|
103
|
-
"summary": "There is no spoon."
|
|
104
|
-
}
|
|
105
|
-
],
|
|
106
|
-
"publications": [
|
|
107
|
-
{
|
|
108
|
-
"name": "Video compression for 3d media",
|
|
109
|
-
"publisher": "Hooli",
|
|
110
|
-
"releaseDate": "2014-10-01",
|
|
111
|
-
"website": "http://en.wikipedia.org/wiki/Silicon_Valley_(TV_series)",
|
|
112
|
-
"summary": "Innovative middle-out compression algorithm that changes the way we store data."
|
|
113
|
-
}
|
|
114
|
-
],
|
|
115
|
-
"skills": [
|
|
116
|
-
{
|
|
117
|
-
"name": "Web Development",
|
|
118
|
-
"level": "Master",
|
|
119
|
-
"keywords": [
|
|
120
|
-
"HTML",
|
|
121
|
-
"CSS",
|
|
122
|
-
"Javascript"
|
|
123
|
-
]
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
"name": "Compression",
|
|
127
|
-
"level": "Master",
|
|
128
|
-
"keywords": [
|
|
129
|
-
"Mpeg",
|
|
130
|
-
"MP4",
|
|
131
|
-
"GIF"
|
|
132
|
-
]
|
|
133
|
-
}
|
|
134
|
-
],
|
|
135
|
-
"languages": [
|
|
136
|
-
{
|
|
137
|
-
"language": "English",
|
|
138
|
-
"fluency": "Native speaker"
|
|
139
|
-
}
|
|
140
|
-
],
|
|
141
|
-
"interests": [
|
|
142
|
-
{
|
|
143
|
-
"name": "Wildlife",
|
|
144
|
-
"keywords": [
|
|
145
|
-
"Ferrets",
|
|
146
|
-
"Unicorns"
|
|
147
|
-
]
|
|
148
|
-
}
|
|
149
|
-
],
|
|
150
|
-
"references": [
|
|
151
|
-
{
|
|
152
|
-
"name": "Erlich Bachman",
|
|
153
|
-
"reference": "It is my pleasure to recommend Richard. That is all."
|
|
154
|
-
}
|
|
155
|
-
]
|
|
156
|
-
}
|