-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathlib.ts
282 lines (268 loc) · 7.58 KB
/
lib.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import puppeteer from 'puppeteer';
import hbs from 'handlebars';
import sass from 'sass';
import yaml from 'yaml';
import { stat, readFile, readdir, writeFile } from 'fs/promises';
import path from 'path';
import descFixer from './handlebars/helpers/descFixer';
import stringify from './handlebars/helpers/stringify';
import base64Encode from './handlebars/helpers/base64Encode';
import { Document } from 'yaml/index';
import type { Arguments } from './index';
/**
* Filterable based on `positions` key
*/
interface FilterableObject {
/**
* List of positions this object is relevant to
*/
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
positions?: string[] | null;
}
interface GenericNamedObject {
/**
* Identifiable name to be rendered in final output
*/
name: string;
}
/**
* An object with `name` that is filterable by `position` CLI arg
*/
interface GenericFilterableNamedObject
extends GenericNamedObject,
FilterableObject {}
/**
* Describes education (usually college level and higher)
*/
interface Education {
/**
* School name
*/
name: string;
/**
* Major/topic of study
*/
major: string;
/**
* Enrollment start/end dates
*/
dates: {
start: number;
end: number;
};
/**
* List of classes taken
*/
classes: GenericFilterableNamedObject[];
}
/**
* Generic resume data
*/
interface ResumeData extends GenericFilterableNamedObject {
/**
* Your (full) name
*/
name: string;
/**
* A bit about you
*/
description: string;
/**
* Location you're looking for/where you currently are
*/
location: {
city: string;
};
/**
* Contact information
*/
contact: {
phone: string;
email: string;
website: string;
github: string;
linkedin: string;
};
/**
* A list of places you went for education
* @see{Education}
*/
education: Education[];
/**
* Places you've worked for/obtained experience from
*/
experience: GenericFilterableNamedObject[];
/**
* Personal projects
*/
projects: GenericFilterableNamedObject[];
/**
* Skills? Things you can market
*/
skills: GenericFilterableNamedObject[];
/**
* Current interests
*/
interests: GenericFilterableNamedObject[];
/**
* Filled in by the -p CLI argument
*/
position?: string;
}
/**
* Parse `argv.data` as YAML, and return elements tagged with `argv.position`
* @param {Arguments} argv - argv object from yargs
* @returns {ResumeData} argv.data as an object, filtered by POSITION
*/
const getYaml = async (argv: Arguments): Promise<ResumeData> => {
const fileStat = await stat(argv.data);
// we'll build the object with user-supplied YAML input, so go ahead and type
// it now
let resumeData = <ResumeData>{};
if (fileStat.isFile()) {
const yamlInput = await readFile(argv.data, { encoding: 'utf8' });
yaml.parseAllDocuments(yamlInput).forEach((parsedDoc: Document) => {
// pretty sure this is expensive, but it's convenient
Object.assign(resumeData, yaml.parse(parsedDoc.toString()));
});
} else if (fileStat.isDirectory()) {
// read all files in dir, combine objects together
const fileList = await readdir(argv.data);
for (const file of fileList) {
// skip non files
if (!(await stat(path.join(argv.data, file))).isFile()) {
continue;
}
const yamlInput = await readFile(path.join(argv.data, file), 'utf8');
yaml.parseAllDocuments(yamlInput).forEach((parsedDoc: Document) => {
// pretty sure this is expensive, but it's convenient
Object.assign(resumeData, yaml.parse(parsedDoc.toString()));
});
}
}
if (argv.position) {
// `as` is used here because this is a filter, but it doesn't delete
// top-level keys
resumeData = filterObj(resumeData, argv.position) as ResumeData;
}
resumeData['position'] = argv.position;
return resumeData;
};
type FilterableAny = FilterableObject | FilterableAny[];
/**
* Filter `elem` for elements with `position`.
* @param {FilterableAny} elem - object to filter
* @param {string} position - tag to filter by
* @returns {FilterableAny | null} `elem` deep-filtered for `position`
*/
const filterObj = (
elem: FilterableAny,
position: string
): FilterableAny | null => {
if (Array.isArray(elem)) {
// if it's an array, you'll need to call this for every elem
return elem.map((e) => filterObj(e, position)).filter((e) => e !== null);
} else if (typeof elem === 'object' && elem !== null) {
// an object. filter all object properties first, because you need to
// recurse through keys before returning.
for (const key in elem) {
elem[key] = filterObj(elem[key], position);
}
// if `elem` is an object and has `position` in `positions` array, return
// it, otherwise set it null...?
if (
Object.prototype.hasOwnProperty.call(elem, 'positions') &&
Array.isArray(elem.positions)
) {
return elem.positions.includes(position) ? elem : null;
}
}
return elem;
};
/**
* compile template with yamlInput
* @param {ResumeData} input - object containing content to pass to template
* @param {string} styles - CSS to pass into {{ styles }} element
* @param {Arguments} argv - argv object from yargs
* @returns {Promise<string>} compiled HTML
*/
const getHbs = async (
input: ResumeData,
styles: string,
argv: Arguments
): Promise<string> => {
const data_path = path.join(argv.template, 'base.hbs');
const data = await readFile(data_path, 'utf8');
return hbs.compile(data)({ ...input, styles });
};
/**
* compile styles
* @param {Arguments} argv - argv object from yargs
* @returns {string} compiled CSS
*/
const getStyles = (argv: Arguments): string => {
const scss_path = path.join(argv.template, 'styles.scss');
return sass.compile(scss_path).css.toString();
};
/**
* Register handlebars helpers
* @param {Arguments} argv - argv object from yargs
*/
const registerHandlebarsHelpers = async (argv: Arguments) => {
// either the assets are inside the dir passed in, or it's next to the data
// file that was passed in, but in either case, use that directory
const asset_dir = (await stat(argv.data)).isDirectory()
? argv.data
: path.dirname(argv.data);
// bind the asset_dir to the first arg of base64Encode
hbs.registerHelper('base64Encode', base64Encode.bind(null, asset_dir));
hbs.registerHelper('descFixer', descFixer);
hbs.registerHelper('stringify', stringify);
};
/**
* template out HTML from variety of sources
* @param {Arguments} argv - argv object from yargs
* @returns {Promise<string>} compiled HTML
*/
const setupHtml = async (argv: Arguments): Promise<string> => {
await registerHandlebarsHelpers(argv);
const data = await getYaml(argv);
const styles = getStyles(argv);
return getHbs(data, styles, argv);
};
/**
* Renders HTML to PDF
* @param {string} html - HTML to render
* @param {Arguments} argv - argv object from yargs
*/
const renderToPDF = async (html: string, argv: Arguments) => {
const browser = await puppeteer.launch({
headless: true,
});
const page = await browser.newPage();
await page.setContent(html, {
waitUntil: 'networkidle2',
});
await page.pdf({
path: argv.output,
format: 'letter',
printBackground: true,
});
await browser.close();
};
/**
* Renders HTML to file
* @param {string} html - HTML to render
* @param {Arguments} argv - argv object from yargs
*/
const renderToHTML = async (html: string, argv: Arguments) => {
return writeFile(argv.output, html);
};
export {
setupHtml,
filterObj,
renderToPDF,
renderToHTML,
GenericFilterableNamedObject,
FilterableAny,
};