kamisaku 0.1.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.
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module CvDataSection
6
+ module ParseHelper
7
+ module Date
8
+ private
9
+
10
+ def to_month(month_int)
11
+ return "" if month_int.nil?
12
+
13
+ ::Date::MONTHNAMES[month_int]
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CvDataSection
4
+ class Skill < Base
5
+ def dig(*path)
6
+ data = super
7
+ return [] if data.nil? && path.first == :items
8
+ data
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "pdfkit"
5
+
6
+ module Kamisaku
7
+ class CvGenerator
8
+ attr_reader :pdf_location, :html_location, :cv_data, :template
9
+
10
+ def initialize(cv_data, pdf_location:, html_location: nil, template: nil)
11
+ @cv_data = cv_data
12
+ @pdf_location = pdf_location
13
+ @html_location = html_location
14
+ @template = template || "sleek"
15
+ end
16
+
17
+ def generate
18
+ generated_html_file do |file_path|
19
+ FileUtils.cp(file_path, html_location) if html_location
20
+ html_file_to_pdf_file(file_path, pdf_location)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def html_file_to_pdf_file(html_file_path, pdf_file_path)
27
+ # Convert the HTML file to a PDF using Google Chrome in headless mode
28
+ pdf_conversion_command = <<~CMD
29
+ google-chrome --headless --disable-gpu --run-all-compositor-stages-before-draw \
30
+ --print-to-pdf=#{pdf_file_path} --no-pdf-header-footer \
31
+ #{html_file_path}
32
+ CMD
33
+
34
+ system(pdf_conversion_command)
35
+ raise "PDF generation failed" unless File.exist?(pdf_file_path)
36
+ end
37
+
38
+ def generated_html_file
39
+ temp_html_file = Tempfile.new(%w[kamisaku .html])
40
+ temp_html_file.write(cv_html)
41
+ temp_html_file.close
42
+ begin
43
+ yield temp_html_file.path
44
+ ensure
45
+ temp_html_file.unlink
46
+ end
47
+ end
48
+
49
+ def cv_html
50
+ rhtml = ERB.new(template_html)
51
+ rhtml.result(@cv_data.get_bindings)
52
+ end
53
+
54
+ def template_html
55
+ path = File.join(File.dirname(__FILE__), "/../templates/#{template}/template.html.erb")
56
+ File.read(path)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Kamisaku
6
+ class MetaFileParser
7
+ attr_reader :location
8
+
9
+ def initialize(location)
10
+ @location = location
11
+ end
12
+
13
+ def validate!
14
+ # TODO: Check if file contains correct fields
15
+ end
16
+
17
+ def parse!
18
+ validate!
19
+ CvData.new(yaml)
20
+ end
21
+
22
+ def yaml
23
+ @yaml ||= YAML.load_file(location)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kamisaku
4
+ VERSION = "0.1.0"
5
+ end
data/lib/kamisaku.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "kamisaku/version"
4
+ require_relative "kamisaku/cv_data_sections/parse_helpers/month_parser"
5
+ require_relative "kamisaku/cv_data_sections/base"
6
+ require_relative "kamisaku/cv_data_sections/skill"
7
+ require_relative "kamisaku/cv_data_sections/interest"
8
+ require_relative "kamisaku/cv_data_sections/experience"
9
+ require_relative "kamisaku/cv_data_sections/education"
10
+ require_relative "kamisaku/cv_data"
11
+ require_relative "kamisaku/meta_file_parser"
12
+ require_relative "kamisaku/cv_generator"
13
+ require_relative "kamisaku/arg_parser"
14
+
15
+ module Kamisaku
16
+ class Error < StandardError; end
17
+
18
+ class Builder
19
+ def run
20
+ options = Kamisaku::ArgParser.parse!
21
+ raise Error.new "CV info file does not exist" unless File.exist?(options[:cv_info])
22
+
23
+ parser = MetaFileParser.new(options[:cv_info])
24
+ cv_data = parser.parse!
25
+ CvGenerator.new(cv_data, pdf_location: options[:output_path], html_location: options[:html_output], template: options[:template]).generate
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,409 @@
1
+ <html>
2
+ <head>
3
+ <meta content="text/html; charset=UTF-8" http-equiv="content-type">
4
+ <style>
5
+ /* latin */
6
+ @font-face {
7
+ font-family: 'Cambria';
8
+ font-style: normal;
9
+ font-weight: 400;
10
+ src: url(https://fonts.gstatic.com/l/font?kit=GFDqWAB9jnWLT-HIK7ILrphaOAw&skey=d4699178559bc4b0&v=v18) format('woff2');
11
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
12
+ }
13
+
14
+ /* latin */
15
+ @font-face {
16
+ font-family: 'Cambria';
17
+ font-style: normal;
18
+ font-weight: 700;
19
+ src: url(https://fonts.gstatic.com/l/font?kit=GFDvWAB9jnWLT-HIIwkuu7V4NSY7WWA&skey=5202a3b6f5388b49&v=v18) format('woff2');
20
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
21
+ }
22
+
23
+ /* cyrillic */
24
+ @font-face {
25
+ font-family: 'Garamond';
26
+ font-style: normal;
27
+ font-weight: 400;
28
+ src: url(https://fonts.gstatic.com/l/font?kit=XoHl2Y_-T6Oo88RDZSQOn9IDlDLo&skey=a3a4b3361b12223a&v=v18) format('woff2');
29
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
30
+ }
31
+
32
+ /* greek */
33
+ @font-face {
34
+ font-family: 'Garamond';
35
+ font-style: normal;
36
+ font-weight: 400;
37
+ src: url(https://fonts.gstatic.com/l/font?kit=XoHl2Y_-T6Oo88RDZSQJn9IDlDLo&skey=a3a4b3361b12223a&v=v18) format('woff2');
38
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
39
+ }
40
+
41
+ /* latin-ext */
42
+ @font-face {
43
+ font-family: 'Garamond';
44
+ font-style: normal;
45
+ font-weight: 400;
46
+ src: url(https://fonts.gstatic.com/l/font?kit=XoHl2Y_-T6Oo88RDZSQEn9IDlDLo&skey=a3a4b3361b12223a&v=v18) format('woff2');
47
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
48
+ }
49
+
50
+ /* latin */
51
+ @font-face {
52
+ font-family: 'Garamond';
53
+ font-style: normal;
54
+ font-weight: 400;
55
+ src: url(https://fonts.gstatic.com/l/font?kit=XoHl2Y_-T6Oo88RDZSQKn9IDlDLo&skey=a3a4b3361b12223a&v=v18) format('woff2');
56
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
57
+ }
58
+
59
+ /* cyrillic */
60
+ @font-face {
61
+ font-family: 'Garamond';
62
+ font-style: normal;
63
+ font-weight: 700;
64
+ src: url(https://fonts.gstatic.com/l/font?kit=XoHi2Y_-T6Oo88RDZSyxuscqtj_C8ibe&skey=66cb3cbedfd2039d&v=v18) format('woff2');
65
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
66
+ }
67
+
68
+ /* greek */
69
+ @font-face {
70
+ font-family: 'Garamond';
71
+ font-style: normal;
72
+ font-weight: 700;
73
+ src: url(https://fonts.gstatic.com/l/font?kit=XoHi2Y_-T6Oo88RDZSyxuscttj_C8ibe&skey=66cb3cbedfd2039d&v=v18) format('woff2');
74
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
75
+ }
76
+
77
+ /* latin-ext */
78
+ @font-face {
79
+ font-family: 'Garamond';
80
+ font-style: normal;
81
+ font-weight: 700;
82
+ src: url(https://fonts.gstatic.com/l/font?kit=XoHi2Y_-T6Oo88RDZSyxuscgtj_C8ibe&skey=66cb3cbedfd2039d&v=v18) format('woff2');
83
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
84
+ }
85
+
86
+ /* latin */
87
+ @font-face {
88
+ font-family: 'Garamond';
89
+ font-style: normal;
90
+ font-weight: 700;
91
+ src: url(https://fonts.gstatic.com/l/font?kit=XoHi2Y_-T6Oo88RDZSyxuscutj_C8ibe&skey=66cb3cbedfd2039d&v=v18) format('woff2');
92
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
93
+ }
94
+
95
+ @page {
96
+ margin: 0;
97
+ padding: 0.5in;
98
+ size: 8.3in 11.7in;
99
+ }
100
+
101
+ body {
102
+ font-family: "Cambria", serif;
103
+ color: #000;
104
+ font-size: 12pt;
105
+ }
106
+
107
+ ul {
108
+ padding-left: 20px;
109
+ margin-top: 2pt;
110
+ }
111
+
112
+ li {
113
+
114
+ }
115
+
116
+ a {
117
+ color: #000000;
118
+ }
119
+
120
+ a:link {
121
+ text-decoration: none;
122
+ }
123
+
124
+ a:visited {
125
+ text-decoration: none;
126
+ }
127
+
128
+ a:hover {
129
+ text-decoration: none;
130
+ }
131
+
132
+ a:active {
133
+ text-decoration: none;
134
+ }
135
+
136
+ .bottom-ruler {
137
+ border-bottom-color: #000000;
138
+ border-bottom-width: 0.8pt;
139
+ padding-bottom: 2pt;
140
+ border-bottom-style: solid;
141
+ }
142
+
143
+ .upcase {
144
+ text-transform: uppercase;
145
+ }
146
+
147
+ .flex {
148
+ display: flex;
149
+ }
150
+
151
+ .flex-grow {
152
+ flex-grow: 1;
153
+ }
154
+
155
+ .sb {
156
+ justify-content: space-between;
157
+ }
158
+
159
+ .contact-section {
160
+ .name {
161
+ font-size: 24pt;
162
+ font-weight: 700;
163
+ font-family: "Garamond";
164
+ }
165
+
166
+ .contact {
167
+ font-size: 16pt;
168
+ font-weight: 400;
169
+ font-family: "Garamond";
170
+ }
171
+ }
172
+
173
+ .summary {
174
+ margin-top: 12pt;
175
+
176
+ .section-title {
177
+ font-weight: bold;
178
+ }
179
+
180
+ .about {
181
+ color: #000000;
182
+ font-weight: 400;
183
+ text-decoration: none;
184
+ vertical-align: baseline;
185
+ font-size: 12pt;
186
+ font-family: "Garamond";
187
+ font-style: normal;
188
+ text-align: justify;
189
+ }
190
+
191
+ }
192
+
193
+ .work-experience {
194
+ margin-top: 12pt;
195
+
196
+ .section-title {
197
+ font-weight: bold;
198
+ }
199
+
200
+ .experience {
201
+ margin-top: 12pt;
202
+
203
+ .organisation-name, .time-period {
204
+ font-weight: 700;
205
+ font-family: "Garamond";
206
+ }
207
+
208
+ .achievements {
209
+ .achievement-item {
210
+ color: #000000;
211
+ font-weight: 400;
212
+ text-decoration: none;
213
+ vertical-align: baseline;
214
+ font-size: 12pt;
215
+ font-family: "Garamond";
216
+ font-style: normal;
217
+ text-align: justify;
218
+ }
219
+ }
220
+
221
+ .job-title, .job-location {
222
+ font-style: italic;
223
+ font-weight: 400;
224
+ font-family: "Garamond";
225
+ }
226
+ }
227
+ }
228
+
229
+ .education {
230
+ margin-top: 12pt;
231
+
232
+ .section-title {
233
+ font-weight: bold;
234
+ }
235
+
236
+ .education-item {
237
+ margin-top: 12pt;
238
+
239
+ .institute-name, .time-period {
240
+ font-weight: 700;
241
+ font-family: "Garamond";
242
+ }
243
+
244
+ .qualification, .education-location {
245
+ font-style: italic;
246
+ font-weight: 400;
247
+ font-family: "Garamond";
248
+ }
249
+
250
+ .achievements {
251
+ .achievement-item {
252
+ color: #000000;
253
+ font-weight: 400;
254
+ text-decoration: none;
255
+ vertical-align: baseline;
256
+ font-size: 12pt;
257
+ font-family: "Garamond";
258
+ font-style: normal;
259
+ text-align: justify;
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ .generic-section {
266
+ margin-top: 12pt;
267
+
268
+ .section-title {
269
+ font-weight: bold;
270
+ margin-bottom: 12pt;
271
+ }
272
+
273
+ .item {
274
+ color: #000000;
275
+ font-weight: 400;
276
+ text-decoration: none;
277
+ vertical-align: baseline;
278
+ font-size: 12pt;
279
+ font-family: "Garamond";
280
+ }
281
+ }
282
+ </style>
283
+ </head>
284
+ <body>
285
+ <div class="bottom-ruler contact-section">
286
+ <div class="name bottom-ruler"><%= dig :profile, :name %></div>
287
+ <div class="contact">
288
+ <% if has? :contact, :email %>
289
+ <a href="mailto:<%= dig :contact, :email %>"><%= dig :contact, :email %></a> |
290
+ <% end %>
291
+ <% if has? :contact, :mobile %>
292
+ <a href="tel:<%= dig :contact, :mobile %>"><%= dig :contact, :mobile %></a> |
293
+ <% end %>
294
+ <% if has?(:contact, :location, :city) || has?(:contact, :location, :country) %>
295
+ <%= dig :contact, :location, :city %>, <%= dig :contact, :location, :country %> |
296
+ <% end %>
297
+ <% if has? :contact, :website %>
298
+ <a href="<%= dig :contact, :website %>"><%= dig :contact, :website %></a> |
299
+ <% end %>
300
+ <% if has? :contact, :github %>
301
+ <a href="https://github.com/<%= dig :contact, :github %>">github.com/<%= dig :contact, :github %></a>
302
+ <% end %>
303
+ </div>
304
+ </div>
305
+
306
+ <div class="summary">
307
+ <div class="upcase bottom-ruler section-title">Summary</div>
308
+ <div class="about"><%= dig :profile, :about %></div>
309
+ </div>
310
+
311
+ <% if has?(:experiences) %>
312
+ <div class="work-experience">
313
+ <div class="upcase bottom-ruler section-title">Work Experience</div>
314
+ <% dig :experiences do |experience| %>
315
+ <div class="experience">
316
+ <div class="flex flex-grow sb">
317
+ <div class="organisation-name"><%= dig experience, :organisation %></div>
318
+ <div class="time-period">
319
+ <%= "#{dig experience, :from, :month} #{dig experience, :from, :year}" %> -
320
+ <%= has?(experience, :to) ? "#{dig experience, :to, :month} #{dig experience, :to, :year}" : "Present" %>
321
+ </div>
322
+ </div>
323
+
324
+ <div class="flex flex-grow sb">
325
+ <div class="job-title"><%= dig experience, :title %></div>
326
+ <div class="job-location">
327
+ <%= "#{dig(experience, :location, :city)}, #{dig(experience, :location, :country)}" %>
328
+ </div>
329
+ </div>
330
+
331
+ <% if has? experience, :achievements %>
332
+ <ul class="achievements">
333
+ <% dig(experience, :achievements).each do |achievement| %>
334
+ <li class="achievement-item"><%= achievement %></li>
335
+ <% end %>
336
+ </ul>
337
+ <% end %>
338
+ </div>
339
+ <% end %>
340
+ </div>
341
+ <% end %>
342
+
343
+ <% if has?(:education) %>
344
+ <div class="education">
345
+ <div class="upcase bottom-ruler section-title">Education</div>
346
+ <% dig :education do |education| %>
347
+ <div class="education-item">
348
+ <div class="flex flex-grow sb">
349
+ <div class="institute-name">
350
+ <%= dig education, :institute %>
351
+ </div>
352
+ <div class="time-period">
353
+ <%= has?(education, :from) ? "#{dig education, :from, :month} #{dig education, :from, :year} - " : "" %>
354
+ <%= has?(education, :to) ? "#{dig education, :to, :month} #{dig education, :to, :year}" : "Present" %>
355
+ </div>
356
+ </div>
357
+
358
+ <div class="flex flex-grow sb">
359
+ <div class="qualification"><%= dig education, :qualification %></div>
360
+ <div class="education-location">
361
+ <%= dig education, :location, :city %>, <%= dig education, :location, :country %>
362
+ </div>
363
+ </div>
364
+ <% if has?(education, :achievements) %>
365
+ <ul class="achievements">
366
+ <% dig(education, :achievements).each do |achievement| %>
367
+ <li class="achievement-item"><%= achievement %></li>
368
+ <% end %>
369
+ </ul>
370
+ <% end %>
371
+ </div>
372
+ <% end %>
373
+ </div>
374
+ <% end %>
375
+
376
+ <% generic_sections = [has?(:skills) ? 'Skills' : nil, has?(:interests) ? 'Interests' : nil].compact %>
377
+
378
+ <% unless generic_sections.empty? %>
379
+
380
+ <div class="generic-section">
381
+ <div class="upcase bottom-ruler section-title"><%= generic_sections.join(', ') %></div>
382
+ <ul>
383
+ <% if has?(:skills) %>
384
+ <li class="item">
385
+ <% if generic_sections.length > 1 %>
386
+ <b>Skills: </b>
387
+ <% end %>
388
+ <% skill_list = [] %>
389
+ <% dig :skills do |skill| %>
390
+ <% dig(skill, :items).each { |item| skill_list.push(item) } %>
391
+ <% end %>
392
+
393
+ <%= skill_list.join('; ') %>
394
+ </li>
395
+ <% end %>
396
+
397
+ <% if has?(:interests) %>
398
+ <li class="item">
399
+ <% if generic_sections.length > 1 %>
400
+ <b>Interests: </b>
401
+ <% end %>
402
+ <%= dig(:interests).join('; ') %>
403
+ </li>
404
+ <% end %>
405
+ </ul>
406
+ </div>
407
+ <% end %>
408
+ </body>
409
+ </html>