kamisaku 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +1 -1
- data/lib/kamisaku/arg_parser.rb +7 -7
- data/lib/kamisaku/chrome_runner.rb +18 -0
- data/lib/kamisaku/cli_runner.rb +14 -0
- data/lib/kamisaku/content_validator.rb +173 -0
- data/lib/kamisaku/errors.rb +3 -0
- data/lib/kamisaku/helpers.rb +15 -0
- data/lib/kamisaku/html_builder.rb +27 -0
- data/lib/kamisaku/pdf.rb +20 -31
- data/lib/kamisaku/template_helpers.rb +11 -0
- data/lib/kamisaku/version.rb +1 -1
- data/lib/kamisaku.rb +7 -26
- data/lib/templates/paper/template.html.erb +63 -59
- data/lib/templates/sleek/template.html.erb +32 -32
- metadata +8 -10
- data/lib/kamisaku/cv_data.rb +0 -56
- data/lib/kamisaku/cv_data_sections/base.rb +0 -24
- data/lib/kamisaku/cv_data_sections/education.rb +0 -15
- data/lib/kamisaku/cv_data_sections/experience.rb +0 -15
- data/lib/kamisaku/cv_data_sections/interest.rb +0 -9
- data/lib/kamisaku/cv_data_sections/parse_helpers/month_parser.rb +0 -17
- data/lib/kamisaku/cv_data_sections/skill.rb +0 -11
- data/lib/kamisaku/cv_generator.rb +0 -66
- data/lib/kamisaku/meta_file_parser.rb +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '08311183150f22bf9897f33d7ecb93616573e99b90a49455efd27747316b3a5b'
|
|
4
|
+
data.tar.gz: b198531b11fdf5b409c1b9c39be5ca04c64a1b9174a867f38e93f7d928f534e9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 621551dfa5fa5bf482ccccf5e8fb72a7a12b2736ed397f8a813f85274f8f33db09e1ffed4d1846dbd3dbb75f2ffc7abb53dd719229c1d00946945050949517d8
|
|
7
|
+
data.tar.gz: 6434717f692966c901becca7a7573ee69ed69b63516bdf356ea826c7d27302255dad520e72ac86519b87213ade8319d3a064de0f9991c5cf69f3242a3918aa20
|
data/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
13
13
|
|
|
14
14
|
### Removed
|
|
15
15
|
|
|
16
|
+
## [0.3.1] - 2025-06-01
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Refactor code and update `Kamisaku::PDF` interface
|
|
20
|
+
|
|
21
|
+
## [0.3.0] - 2025-05-28
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- Add `Kamisaku::PDF` interface that can be used externally to generate PDF
|
|
25
|
+
|
|
16
26
|
## [0.2.1] - 2025-05-28
|
|
17
27
|
|
|
18
28
|
### Changed
|
data/Gemfile.lock
CHANGED
data/lib/kamisaku/arg_parser.rb
CHANGED
|
@@ -12,21 +12,21 @@ module Kamisaku
|
|
|
12
12
|
options[:html_output] = html_output
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
parser.on("-c", "--
|
|
16
|
-
options[:
|
|
15
|
+
parser.on("-c", "--yaml-file INFO", "Require the INFO") do |yaml_file|
|
|
16
|
+
options[:yaml_file] = yaml_file
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
parser.on("-o", "--
|
|
20
|
-
options[:
|
|
19
|
+
parser.on("-o", "--pdf-file OUTPUT", "Require the OUTPUT") do |pdf_file|
|
|
20
|
+
options[:pdf_file] = pdf_file
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
parser.on("-t", "--template
|
|
23
|
+
parser.on("-t", "--template TEMPLATE", "Provide the TEMPLATE name") do |template|
|
|
24
24
|
options[:template] = template
|
|
25
25
|
end
|
|
26
26
|
end.parse!
|
|
27
27
|
|
|
28
|
-
raise OptionParser::MissingArgument.new("
|
|
29
|
-
raise OptionParser::MissingArgument.new("
|
|
28
|
+
raise OptionParser::MissingArgument.new("-c") if options[:yaml_file].nil?
|
|
29
|
+
raise OptionParser::MissingArgument.new("-o") if options[:pdf_file].nil?
|
|
30
30
|
|
|
31
31
|
options
|
|
32
32
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Kamisaku
|
|
2
|
+
class ChromeRunner
|
|
3
|
+
def initialize(sandbox: false, headless: true, gpu: false)
|
|
4
|
+
@base_args = ["--run-all-compositor-stages-before-draw"]
|
|
5
|
+
@base_args << "--no-sandbox" unless sandbox
|
|
6
|
+
@base_args << "--headless" if headless
|
|
7
|
+
@base_args << "--disable-gpu" unless gpu
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def html_to_pdf(html_path, pdf_path, pdf_header_footer: false)
|
|
11
|
+
args = [*@base_args]
|
|
12
|
+
args << "--no-pdf-header-footer" unless pdf_header_footer
|
|
13
|
+
args << "--print-to-pdf=#{pdf_path}"
|
|
14
|
+
args << html_path
|
|
15
|
+
system("google-chrome", *args)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Kamisaku
|
|
2
|
+
class CLIRunner
|
|
3
|
+
def self.run
|
|
4
|
+
options = Kamisaku::ArgParser.parse!
|
|
5
|
+
yaml_file = options[:yaml_file]
|
|
6
|
+
raise Kamisaku::Error.new "YAML file does not exist" unless File.exist?(yaml_file)
|
|
7
|
+
|
|
8
|
+
yaml_string = File.read(yaml_file)
|
|
9
|
+
pdf = PDF.new(content_hash: Helpers.yaml_str_to_content_hash(yaml_string), template: options[:template])
|
|
10
|
+
pdf.write_to(options[:pdf_file])
|
|
11
|
+
pdf.generate_html(options[:html_output]) if options[:html_output]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
module Kamisaku
|
|
2
|
+
class ContentValidator
|
|
3
|
+
attr_reader :content_hash
|
|
4
|
+
alias_method :data, :content_hash
|
|
5
|
+
|
|
6
|
+
def initialize(content_hash:)
|
|
7
|
+
@content_hash = content_hash
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def validate!
|
|
11
|
+
validate_version
|
|
12
|
+
validate_profile
|
|
13
|
+
validate_contact
|
|
14
|
+
validate_skills
|
|
15
|
+
validate_experiences
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def validate_version
|
|
21
|
+
raise Error, "Invalid version" unless data[:version] && data[:version] == 1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate_profile
|
|
25
|
+
raise Error, "Missing profile" unless data[:profile]
|
|
26
|
+
raise Error, "Profile must be a hash" unless data[:profile].is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
allowed_fields = %i[name title about]
|
|
29
|
+
profile_fields = data[:profile].keys
|
|
30
|
+
|
|
31
|
+
unless profile_fields.all? { |field| allowed_fields.include?(field) }
|
|
32
|
+
raise Error, "Profile contains invalid fields"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
unless profile_fields.size == allowed_fields.size
|
|
36
|
+
raise Error, "Profile must contain exactly the fields: #{allowed_fields.join(", ")}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
data[:profile].each do |field, value|
|
|
40
|
+
unless value.is_a?(String)
|
|
41
|
+
raise Error, "Profile field '#{field}' must be a string"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_contact
|
|
47
|
+
raise Error, "Missing contact" unless data[:contact]
|
|
48
|
+
raise Error, "Contact must be a hash" unless data[:contact].is_a?(Hash)
|
|
49
|
+
|
|
50
|
+
allowed_fields = %i[github mobile linkedin website email location]
|
|
51
|
+
contact_fields = data[:contact].keys
|
|
52
|
+
|
|
53
|
+
unless contact_fields.all? { |field| allowed_fields.include?(field) }
|
|
54
|
+
raise Error, "Contact contains invalid fields"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
allowed_fields.each do |field|
|
|
58
|
+
raise Error, "Contact missing required field '#{field}'" unless contact_fields.include?(field)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
data[:contact].each do |field, value|
|
|
62
|
+
if field == :location
|
|
63
|
+
validate_location(value, "Contact section")
|
|
64
|
+
else
|
|
65
|
+
raise Error, "Contact field '#{field}' must be a string" unless value.is_a?(String)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_location(location, section)
|
|
71
|
+
raise Error, "#{section}: Location must be a hash" unless location.is_a?(Hash)
|
|
72
|
+
|
|
73
|
+
allowed_fields = %i[country city]
|
|
74
|
+
location_fields = location.keys
|
|
75
|
+
|
|
76
|
+
unless location_fields.all? { |field| allowed_fields.include?(field) }
|
|
77
|
+
raise Error, "#{section}: Location contains invalid fields"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
allowed_fields.each do |field|
|
|
81
|
+
raise Error, "#{section}: Location missing required field '#{field}'" unless location_fields.include?(field)
|
|
82
|
+
raise Error, "#{section}: Location field '#{field}' must be a string" unless location[field].is_a?(String)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def validate_skills
|
|
87
|
+
return unless data[:skills]
|
|
88
|
+
|
|
89
|
+
raise Error, "Skills must be an array" unless data[:skills].is_a?(Array)
|
|
90
|
+
|
|
91
|
+
data[:skills].each do |skill|
|
|
92
|
+
raise Error, "Each skill must be a hash" unless skill.is_a?(Hash)
|
|
93
|
+
|
|
94
|
+
allowed_fields = %i[area items]
|
|
95
|
+
skill_fields = skill.keys
|
|
96
|
+
|
|
97
|
+
unless skill_fields.all? { |field| allowed_fields.include?(field) }
|
|
98
|
+
raise Error, "Skills section: Skill contains invalid fields"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
allowed_fields.each do |field|
|
|
102
|
+
raise Error, "Skills section: Skill missing required field '#{field}'" unless skill_fields.include?(field)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
raise Error, "Skills section: Skill field 'area' must be a string" unless skill[:area].is_a?(String)
|
|
106
|
+
raise Error, "Skills section: Skill field 'items' must be an array" unless skill[:items].is_a?(Array)
|
|
107
|
+
|
|
108
|
+
skill[:items].each do |item|
|
|
109
|
+
raise Error, "Skills section: Each skill item must be a string" unless item.is_a?(String)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def validate_experiences
|
|
115
|
+
return unless data[:experiences]
|
|
116
|
+
|
|
117
|
+
raise Error, "Experiences must be an array" unless data[:experiences].is_a?(Array)
|
|
118
|
+
|
|
119
|
+
data[:experiences].each do |experience|
|
|
120
|
+
raise Error, "Each experience must be a hash" unless experience.is_a?(Hash)
|
|
121
|
+
|
|
122
|
+
allowed_fields = %i[title organisation location from to skills achievements]
|
|
123
|
+
required_fields = %i[title organisation location from skills achievements]
|
|
124
|
+
experience_fields = experience.keys
|
|
125
|
+
|
|
126
|
+
unless experience_fields.all? { |field| allowed_fields.include?(field) }
|
|
127
|
+
raise Error, "Experiences section: Experience contains invalid fields"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
required_fields.each do |field|
|
|
131
|
+
raise Error, "Experiences section: Experience missing required field '#{field}'" unless experience_fields.include?(field)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
raise Error, "Experiences section: Experience field 'title' must be a string" unless experience[:title].is_a?(String)
|
|
135
|
+
raise Error, "Experiences section: Experience field 'organisation' must be a string" unless experience[:organisation].is_a?(String)
|
|
136
|
+
|
|
137
|
+
validate_location(experience[:location], "Experiences section")
|
|
138
|
+
|
|
139
|
+
raise Error, "Experiences section: Experience field 'from' must be a hash" unless experience[:from].is_a?(Hash)
|
|
140
|
+
validate_date_format(experience[:from], "Experiences section")
|
|
141
|
+
|
|
142
|
+
if experience.key?(:to)
|
|
143
|
+
raise Error, "Experiences section: Experience field 'to' must be a hash" unless experience[:to].is_a?(Hash)
|
|
144
|
+
validate_date_format(experience[:to], "Experiences section")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
raise Error, "Experiences section: Experience field 'skills' must be an array" unless experience[:skills].is_a?(Array)
|
|
148
|
+
experience[:skills].each do |skill|
|
|
149
|
+
raise Error, "Experiences section: Each experience skill item must be a string" unless skill.is_a?(String)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
raise Error, "Experiences section: Experience field 'achievements' must be an array" unless experience[:achievements].is_a?(Array)
|
|
153
|
+
experience[:achievements].each do |achievement|
|
|
154
|
+
raise Error, "Experiences section: Each experience achievement item must be a string" unless achievement.is_a?(String)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def validate_date_format(date, section)
|
|
160
|
+
allowed_fields = %i[month year]
|
|
161
|
+
date_fields = date.keys
|
|
162
|
+
|
|
163
|
+
unless date_fields.all? { |field| allowed_fields.include?(field) }
|
|
164
|
+
raise Error, "#{section}: Date contains invalid fields"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
allowed_fields.each do |field|
|
|
168
|
+
raise Error, "#{section}: Date missing required field '#{field}'" unless date_fields.include?(field)
|
|
169
|
+
raise Error, "#{section}: Date field '#{field}' must be an integer" unless date[field].is_a?(Integer)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "psych"
|
|
2
|
+
|
|
3
|
+
module Kamisaku
|
|
4
|
+
module Helpers
|
|
5
|
+
def self.yaml_str_to_content_hash(yaml_str)
|
|
6
|
+
Psych.safe_load(yaml_str, symbolize_names: true, aliases: false, freeze: true)
|
|
7
|
+
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::AliasesNotEnabled => error
|
|
8
|
+
raise Kamisaku::Error.new error.message
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.remove_metadata_from_pdf_file(file_path)
|
|
12
|
+
system("exiftool", "-all=", file_path, "-overwrite_original")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require "erb"
|
|
2
|
+
|
|
3
|
+
module Kamisaku
|
|
4
|
+
class HtmlBuilder
|
|
5
|
+
attr_reader :content_hash, :template
|
|
6
|
+
alias_method :data, :content_hash
|
|
7
|
+
|
|
8
|
+
include TemplateHelpers
|
|
9
|
+
|
|
10
|
+
def initialize(content_hash, template)
|
|
11
|
+
@content_hash = content_hash
|
|
12
|
+
@template = template
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def html
|
|
16
|
+
rhtml = ::ERB.new(template_html)
|
|
17
|
+
rhtml.result(binding)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def template_html
|
|
23
|
+
path = File.join(File.dirname(__FILE__), "/../templates/#{template}/template.html.erb")
|
|
24
|
+
File.read(path)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/kamisaku/pdf.rb
CHANGED
|
@@ -1,47 +1,34 @@
|
|
|
1
1
|
module Kamisaku
|
|
2
2
|
class PDF
|
|
3
|
-
attr_reader :
|
|
3
|
+
attr_reader :content_hash, :template
|
|
4
4
|
|
|
5
|
-
def initialize(
|
|
6
|
-
@
|
|
7
|
-
@cv_data = CvData.new(YAML.load(yaml_content))
|
|
8
|
-
@pdf_location = pdf_location
|
|
9
|
-
@html_location = html_location
|
|
5
|
+
def initialize(content_hash:, template: nil)
|
|
6
|
+
@content_hash = content_hash
|
|
10
7
|
@template = template || "sleek"
|
|
8
|
+
ContentValidator.new(content_hash:).validate!
|
|
11
9
|
end
|
|
12
10
|
|
|
13
|
-
def
|
|
14
|
-
|
|
15
|
-
FileUtils.cp(file_path, html_location) if html_location
|
|
11
|
+
def write_to(pdf_location)
|
|
12
|
+
html_file do |file_path|
|
|
16
13
|
html_file_to_pdf_file(file_path, pdf_location)
|
|
17
|
-
|
|
14
|
+
Helpers.remove_metadata_from_pdf_file(pdf_location)
|
|
18
15
|
end
|
|
19
16
|
end
|
|
20
17
|
|
|
18
|
+
def generate_html(html_location)
|
|
19
|
+
html_file { |file_path| FileUtils.cp(file_path, html_location) }
|
|
20
|
+
end
|
|
21
|
+
|
|
21
22
|
private
|
|
22
23
|
|
|
23
24
|
def html_file_to_pdf_file(html_file_path, pdf_file_path)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
google-chrome --headless --disable-gpu --run-all-compositor-stages-before-draw \
|
|
27
|
-
--print-to-pdf=#{pdf_file_path} --no-pdf-header-footer \
|
|
28
|
-
#{html_file_path}
|
|
29
|
-
CMD
|
|
30
|
-
|
|
31
|
-
system(pdf_conversion_command)
|
|
32
|
-
raise "PDF generation failed" unless File.exist?(pdf_file_path)
|
|
25
|
+
runner = ChromeRunner.new
|
|
26
|
+
runner.html_to_pdf(html_file_path, pdf_file_path)
|
|
33
27
|
end
|
|
34
28
|
|
|
35
|
-
def
|
|
36
|
-
command = <<~CMD
|
|
37
|
-
exiftool -all= #{file_path} -overwrite_original
|
|
38
|
-
CMD
|
|
39
|
-
system(command)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def generated_html_file
|
|
29
|
+
def html_file
|
|
43
30
|
temp_html_file = Tempfile.new(%w[kamisaku .html])
|
|
44
|
-
temp_html_file.write(
|
|
31
|
+
temp_html_file.write(html)
|
|
45
32
|
temp_html_file.close
|
|
46
33
|
begin
|
|
47
34
|
yield temp_html_file.path
|
|
@@ -50,9 +37,11 @@ module Kamisaku
|
|
|
50
37
|
end
|
|
51
38
|
end
|
|
52
39
|
|
|
53
|
-
def
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
def html
|
|
41
|
+
return @html if defined? @html
|
|
42
|
+
|
|
43
|
+
builder = HtmlBuilder.new(content_hash, template)
|
|
44
|
+
@html = builder.html
|
|
56
45
|
end
|
|
57
46
|
|
|
58
47
|
def template_html
|
data/lib/kamisaku/version.rb
CHANGED
data/lib/kamisaku.rb
CHANGED
|
@@ -1,29 +1,10 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
require_relative "kamisaku/version"
|
|
4
|
-
require_relative "kamisaku/
|
|
5
|
-
require_relative "kamisaku/
|
|
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"
|
|
2
|
+
require_relative "kamisaku/errors"
|
|
3
|
+
require_relative "kamisaku/helpers"
|
|
13
4
|
require_relative "kamisaku/arg_parser"
|
|
14
5
|
require_relative "kamisaku/pdf"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def run
|
|
21
|
-
options = Kamisaku::ArgParser.parse!
|
|
22
|
-
raise Error.new "CV info file does not exist" unless File.exist?(options[:cv_info])
|
|
23
|
-
|
|
24
|
-
parser = MetaFileParser.new(options[:cv_info])
|
|
25
|
-
cv_data = parser.parse!
|
|
26
|
-
CvGenerator.new(cv_data, pdf_location: options[:output_path], html_location: options[:html_output], template: options[:template]).generate
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
6
|
+
require_relative "kamisaku/chrome_runner"
|
|
7
|
+
require_relative "kamisaku/cli_runner"
|
|
8
|
+
require_relative "kamisaku/template_helpers"
|
|
9
|
+
require_relative "kamisaku/html_builder"
|
|
10
|
+
require_relative "kamisaku/content_validator"
|
|
@@ -286,58 +286,66 @@
|
|
|
286
286
|
</style>
|
|
287
287
|
</head>
|
|
288
288
|
<body>
|
|
289
|
-
|
|
290
|
-
<div class="
|
|
291
|
-
|
|
292
|
-
<% if
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
289
|
+
<% if (profile = data[:profile]) %>
|
|
290
|
+
<div class="bottom-ruler contact-section">
|
|
291
|
+
<div class="name bottom-ruler"><%= profile.dig(:name) %></div>
|
|
292
|
+
<% if (contact = data[:contact]) %>
|
|
293
|
+
<div class="contact">
|
|
294
|
+
<% if (email = contact[:email]) %>
|
|
295
|
+
<a href="mailto:<%= email %>"><%= email %></a>
|
|
296
|
+
<% end %>
|
|
297
|
+
|
|
298
|
+
<% if (mobile = contact[:mobile]) %>
|
|
299
|
+
<a href="tel:<%= mobile %>"><%= mobile %></a>
|
|
300
|
+
<% end %>
|
|
301
|
+
|
|
302
|
+
<% if (city = contact.dig(:location, :city)) || (country = contact.dig(:location, :country)) %>
|
|
303
|
+
<%= "#{city}, #{country}".strip %>
|
|
304
|
+
<% end %>
|
|
305
|
+
|
|
306
|
+
<% if (website = contact[:website]) %>
|
|
307
|
+
<a href="<%= website %>"><%= website %></a>
|
|
308
|
+
<% end %>
|
|
309
|
+
|
|
310
|
+
<% if (github = contact[:github]) %>
|
|
311
|
+
<a href="https://github.com/<%= github %>">github.com/<%= github %></a>
|
|
312
|
+
<% end %>
|
|
313
|
+
|
|
314
|
+
<% if (linkedin = contact[:linkedin]) %>
|
|
315
|
+
<a href="https://linkedin.com/in/<%= linkedin %>">linkedin.com/in/<%= linkedin %></a>
|
|
316
|
+
<% end %>
|
|
317
|
+
</div>
|
|
309
318
|
<% end %>
|
|
310
319
|
</div>
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
<div class="
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
</div>
|
|
320
|
+
<div class="summary">
|
|
321
|
+
<div class="upcase bottom-ruler section-title">Summary</div>
|
|
322
|
+
<div class="about"><%= profile[:about] %></div>
|
|
323
|
+
</div>
|
|
324
|
+
<% end %>
|
|
317
325
|
|
|
318
|
-
<% if
|
|
326
|
+
<% if (experiences = data[:experiences]) %>
|
|
319
327
|
<div class="work-experience">
|
|
320
328
|
<div class="upcase bottom-ruler section-title">Work Experience</div>
|
|
321
|
-
<%
|
|
329
|
+
<% experiences.each do |experience| %>
|
|
322
330
|
<div class="experience">
|
|
323
331
|
<div class="flex flex-grow sb">
|
|
324
|
-
<div class="organisation-name"><%=
|
|
332
|
+
<div class="organisation-name"><%= experience[:organisation] %></div>
|
|
325
333
|
<div class="time-period">
|
|
326
|
-
<%= "#{
|
|
327
|
-
<%=
|
|
334
|
+
<%= "#{month_name experience.dig(:from, :month)} #{experience.dig(:from, :year)}" %> -
|
|
335
|
+
<%= experience[:to] ? "#{month_name experience.dig(:to, :month)} #{experience.dig(:to, :year)}" : "Present" %>
|
|
328
336
|
</div>
|
|
329
337
|
</div>
|
|
330
338
|
|
|
331
339
|
<div class="flex flex-grow sb">
|
|
332
|
-
<div class="job-title"><%=
|
|
340
|
+
<div class="job-title"><%= experience[:title] %></div>
|
|
333
341
|
<div class="job-location">
|
|
334
|
-
<%= "#{dig(
|
|
342
|
+
<%= "#{experience.dig(:location, :city)}, #{experience.dig(:location, :country)}" %>
|
|
335
343
|
</div>
|
|
336
344
|
</div>
|
|
337
345
|
|
|
338
|
-
<% if
|
|
346
|
+
<% if (achievements = experience[:achievements]) %>
|
|
339
347
|
<ul class="achievements">
|
|
340
|
-
<%
|
|
348
|
+
<% achievements.each do |achievement| %>
|
|
341
349
|
<li class="achievement-item"><%= achievement %></li>
|
|
342
350
|
<% end %>
|
|
343
351
|
</ul>
|
|
@@ -347,30 +355,31 @@
|
|
|
347
355
|
</div>
|
|
348
356
|
<% end %>
|
|
349
357
|
|
|
350
|
-
<% if
|
|
358
|
+
<% if (education_list = data[:education]) %>
|
|
351
359
|
<div class="education">
|
|
352
360
|
<div class="upcase bottom-ruler section-title">Education</div>
|
|
353
|
-
<%
|
|
361
|
+
<% education_list.each do |education| %>
|
|
354
362
|
<div class="education-item">
|
|
355
363
|
<div class="flex flex-grow sb">
|
|
356
364
|
<div class="institute-name">
|
|
357
|
-
<%=
|
|
365
|
+
<%= education[:institute] %>
|
|
358
366
|
</div>
|
|
359
367
|
<div class="time-period">
|
|
360
|
-
<%=
|
|
361
|
-
<%=
|
|
368
|
+
<%= education.dig(:from) ? "#{month_name education.dig(:from, :month)} #{education.dig(:from, :year)} - " : "" %>
|
|
369
|
+
<%= education.dig(:to) ? "#{month_name education.dig(:to, :month)} #{education.dig(:to, :year)}" : "Present" %>
|
|
362
370
|
</div>
|
|
363
371
|
</div>
|
|
364
372
|
|
|
365
373
|
<div class="flex flex-grow sb">
|
|
366
|
-
<div class="qualification"><%=
|
|
374
|
+
<div class="qualification"><%= education[:qualification] %></div>
|
|
367
375
|
<div class="education-location">
|
|
368
|
-
<%= dig
|
|
376
|
+
<%= "#{education.dig(:location, :city)}, #{education.dig(:location, :country)}".strip %>
|
|
369
377
|
</div>
|
|
370
378
|
</div>
|
|
371
|
-
|
|
379
|
+
|
|
380
|
+
<% if (achievements = education[:achievements]) %>
|
|
372
381
|
<ul class="achievements">
|
|
373
|
-
<%
|
|
382
|
+
<% achievements.each do |achievement| %>
|
|
374
383
|
<li class="achievement-item"><%= achievement %></li>
|
|
375
384
|
<% end %>
|
|
376
385
|
</ul>
|
|
@@ -380,37 +389,32 @@
|
|
|
380
389
|
</div>
|
|
381
390
|
<% end %>
|
|
382
391
|
|
|
383
|
-
<% generic_sections = [has?(:skills) ? 'Skills' : nil, has?(:interests) ? 'Interests' : nil].compact %>
|
|
384
|
-
|
|
385
|
-
<% unless generic_sections.empty? %>
|
|
386
392
|
|
|
393
|
+
<% if (generic_sections = data.slice(:skills, :interests).compact) %>
|
|
394
|
+
<% show_subtitle = generic_sections.keys.size > 1 %>
|
|
387
395
|
<div class="generic-section">
|
|
388
|
-
<div class="upcase bottom-ruler section-title"><%= generic_sections.join(', ') %></div>
|
|
396
|
+
<div class="upcase bottom-ruler section-title"><%= generic_sections.keys.map(&:capitalize).join(', ') %></div>
|
|
389
397
|
<ul>
|
|
390
|
-
<% if
|
|
398
|
+
<% if (skills = generic_sections[:skills]) %>
|
|
391
399
|
<li class="item">
|
|
392
|
-
<% if
|
|
400
|
+
<% if show_subtitle %>
|
|
393
401
|
<b>Skills: </b>
|
|
394
402
|
<% end %>
|
|
395
|
-
|
|
396
|
-
<% dig :skills do |skill| %>
|
|
397
|
-
<% dig(skill, :items).each { |item| skill_list.push(item) } %>
|
|
398
|
-
<% end %>
|
|
399
|
-
|
|
400
|
-
<%= skill_list.join('; ') %>
|
|
403
|
+
<%= skills.flat_map { |skill| skill[:items] }.join('; ') %>
|
|
401
404
|
</li>
|
|
402
405
|
<% end %>
|
|
403
406
|
|
|
404
|
-
<% if
|
|
407
|
+
<% if (interests = generic_sections[:interests]) %>
|
|
405
408
|
<li class="item">
|
|
406
|
-
<% if
|
|
409
|
+
<% if show_subtitle %>
|
|
407
410
|
<b>Interests: </b>
|
|
408
411
|
<% end %>
|
|
409
|
-
<%=
|
|
412
|
+
<%= interests.join('; ') %>
|
|
410
413
|
</li>
|
|
411
414
|
<% end %>
|
|
412
415
|
</ul>
|
|
413
416
|
</div>
|
|
414
417
|
<% end %>
|
|
418
|
+
|
|
415
419
|
</body>
|
|
416
420
|
</html>
|
|
@@ -133,39 +133,39 @@
|
|
|
133
133
|
<div class="profile">
|
|
134
134
|
<div class="flex">
|
|
135
135
|
<div>
|
|
136
|
-
<div class="name"> <%= dig
|
|
137
|
-
<div class="title"> <%= dig
|
|
136
|
+
<div class="name"> <%= data.dig(:profile, :name) %></div>
|
|
137
|
+
<div class="title"> <%= data.dig(:profile, :title) %></div>
|
|
138
138
|
</div>
|
|
139
139
|
<div class="flex contact">
|
|
140
140
|
<div style="margin-right: 10px;">
|
|
141
141
|
<div class="contact-item">
|
|
142
142
|
<span class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg></span>
|
|
143
|
-
<span><a href="https://github.com/<%= dig
|
|
143
|
+
<span><a href="https://github.com/<%= data.dig(:contact, :github) %>">github.com/<%= data.dig(:contact,:github) %></a></span>
|
|
144
144
|
</div>
|
|
145
145
|
|
|
146
146
|
<div class="contact-item">
|
|
147
147
|
<span class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"/></svg></span>
|
|
148
|
-
<span><a href="https://linkedin.com/in/<%= dig :contact, :linkedin %>">linkedin.com/in/<%= dig :contact, :linkedin %></a></span>
|
|
148
|
+
<span><a href="https://linkedin.com/in/<%= data.dig :contact, :linkedin %>">linkedin.com/in/<%= data.dig :contact, :linkedin %></a></span>
|
|
149
149
|
</div>
|
|
150
150
|
|
|
151
151
|
<div class="contact-item">
|
|
152
152
|
<span class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M352 256c0 22.2-1.2 43.6-3.3 64H163.3c-2.2-20.4-3.3-41.8-3.3-64s1.2-43.6 3.3-64H348.7c2.2 20.4 3.3 41.8 3.3 64zm28.8-64H503.9c5.3 20.5 8.1 41.9 8.1 64s-2.8 43.5-8.1 64H380.8c2.1-20.6 3.2-42 3.2-64s-1.1-43.4-3.2-64zm112.6-32H376.7c-10-63.9-29.8-117.4-55.3-151.6c78.3 20.7 142 77.5 171.9 151.6zm-149.1 0H167.7c6.1-36.4 15.5-68.6 27-94.7c10.5-23.6 22.2-40.7 33.5-51.5C239.4 3.2 248.7 0 256 0s16.6 3.2 27.8 13.8c11.3 10.8 23 27.9 33.5 51.5c11.6 26 20.9 58.2 27 94.7zm-209 0H18.6C48.6 85.9 112.2 29.1 190.6 8.4C165.1 42.6 145.3 96.1 135.3 160zM8.1 192H131.2c-2.1 20.6-3.2 42-3.2 64s1.1 43.4 3.2 64H8.1C2.8 299.5 0 278.1 0 256s2.8-43.5 8.1-64zM194.7 446.6c-11.6-26-20.9-58.2-27-94.6H344.3c-6.1 36.4-15.5 68.6-27 94.6c-10.5 23.6-22.2 40.7-33.5 51.5C272.6 508.8 263.3 512 256 512s-16.6-3.2-27.8-13.8c-11.3-10.8-23-27.9-33.5-51.5zM135.3 352c10 63.9 29.8 117.4 55.3 151.6C112.2 482.9 48.6 426.1 18.6 352H135.3zm358.1 0c-30 74.1-93.6 130.9-171.9 151.6c25.5-34.2 45.2-87.7 55.3-151.6H493.4z"/></svg></span>
|
|
153
|
-
<span><a href="https://<%= dig :contact, :website %>"><%= dig :contact, :website %></a></span>
|
|
153
|
+
<span><a href="https://<%= data.dig :contact, :website %>"><%= data.dig :contact, :website %></a></span>
|
|
154
154
|
</div>
|
|
155
155
|
</div>
|
|
156
156
|
|
|
157
157
|
<div>
|
|
158
158
|
<div class="contact-item">
|
|
159
159
|
<span class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M0 64C0 28.7 28.7 0 64 0H256c35.3 0 64 28.7 64 64V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V64zm64 96v64c0 17.7 14.3 32 32 32H224c17.7 0 32-14.3 32-32V160c0-17.7-14.3-32-32-32H96c-17.7 0-32 14.3-32 32zM80 352a24 24 0 1 0 0-48 24 24 0 1 0 0 48zm24 56a24 24 0 1 0 -48 0 24 24 0 1 0 48 0zm56-56a24 24 0 1 0 0-48 24 24 0 1 0 0 48zm24 56a24 24 0 1 0 -48 0 24 24 0 1 0 48 0zm56-56a24 24 0 1 0 0-48 24 24 0 1 0 0 48zm24 56a24 24 0 1 0 -48 0 24 24 0 1 0 48 0zM128 48c-8.8 0-16 7.2-16 16s7.2 16 16 16h64c8.8 0 16-7.2 16-16s-7.2-16-16-16H128z"/></svg></span>
|
|
160
|
-
<span><a href="tel:<%= dig :contact, :mobile %>"><%= dig :contact, :mobile %></a></span>
|
|
160
|
+
<span><a href="tel:<%= data.dig :contact, :mobile %>"><%= data.dig :contact, :mobile %></a></span>
|
|
161
161
|
</div>
|
|
162
162
|
<div class="contact-item">
|
|
163
163
|
<span class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/></svg></span>
|
|
164
|
-
<span><%= dig :contact, :email %></span>
|
|
164
|
+
<span><%= data.dig :contact, :email %></span>
|
|
165
165
|
</div>
|
|
166
166
|
<div class="contact-item">
|
|
167
167
|
<span class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M215.7 499.2C267 435 384 279.4 384 192C384 86 298 0 192 0S0 86 0 192c0 87.4 117 243 168.3 307.2c12.3 15.3 35.1 15.3 47.4 0zM192 128a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"/></svg></span>
|
|
168
|
-
<span><%= dig :contact, :location, :city %>, <%= dig :contact, :location, :country %></span>
|
|
168
|
+
<span><%= data.dig :contact, :location, :city %>, <%= data.dig :contact, :location, :country %></span>
|
|
169
169
|
</div>
|
|
170
170
|
</div>
|
|
171
171
|
|
|
@@ -173,16 +173,16 @@
|
|
|
173
173
|
</div>
|
|
174
174
|
<div class="about">
|
|
175
175
|
<h2 class="upcase heading">Professional Summary</h2>
|
|
176
|
-
<p><%= dig :profile, :about %></p>
|
|
176
|
+
<p><%= data.dig :profile, :about %></p>
|
|
177
177
|
</div>
|
|
178
178
|
</div>
|
|
179
179
|
|
|
180
|
-
<% if
|
|
180
|
+
<% if data[:skills] %>
|
|
181
181
|
<div class="skills">
|
|
182
|
-
<%
|
|
182
|
+
<% data[:skills].each do |skill| %>
|
|
183
183
|
<div>
|
|
184
|
-
<b><%= dig
|
|
185
|
-
<% dig(
|
|
184
|
+
<b><%= skill.dig :area %> </b>
|
|
185
|
+
<% skill.dig(:items).each do |item| %>
|
|
186
186
|
• <%= item %>
|
|
187
187
|
<% end %>
|
|
188
188
|
</div>
|
|
@@ -190,36 +190,36 @@
|
|
|
190
190
|
</div>
|
|
191
191
|
<% end %>
|
|
192
192
|
|
|
193
|
-
<% if
|
|
193
|
+
<% if data[:experiences] %>
|
|
194
194
|
<div class="experiences">
|
|
195
195
|
<h2 class="upcase heading">Work Experience</h2>
|
|
196
|
-
<%
|
|
196
|
+
<% data[:experiences].each do |experience| %>
|
|
197
197
|
<div class="experience">
|
|
198
|
-
<h3 class="job-title"><%= dig(
|
|
198
|
+
<h3 class="job-title"><%= experience.dig(:title) %></h3>
|
|
199
199
|
<div class="flex">
|
|
200
200
|
<div class="flex-grow">
|
|
201
201
|
<span style="font-weight: bold">
|
|
202
|
-
<%= dig(
|
|
202
|
+
<%= experience.dig(:organisation) %>
|
|
203
203
|
</span> /
|
|
204
|
-
<span><%= "#{dig(
|
|
204
|
+
<span><%= "#{experience.dig(:location, :city)}, #{experience.dig(:location, :country)}" %></span>
|
|
205
205
|
</div>
|
|
206
206
|
<div class="time-period">
|
|
207
207
|
<span class="time">
|
|
208
|
-
<%= dig(
|
|
209
|
-
<%=
|
|
208
|
+
<%= month_name experience.dig(:from, :month) %> <%= experience.dig(:from, :year) %> -
|
|
209
|
+
<%= experience[:to] ? "#{month_name experience.dig(:to, :month)} #{experience.dig(:to, :year)}" : 'Present' %>
|
|
210
210
|
</span>
|
|
211
211
|
</div>
|
|
212
212
|
</div>
|
|
213
|
-
<% if
|
|
213
|
+
<% if experience[:achievements] %>
|
|
214
214
|
<div class="achievements">
|
|
215
|
-
<% dig(
|
|
215
|
+
<% experience.dig(:achievements).each do |achievement| %>
|
|
216
216
|
<div class="achievement-item">• <%= achievement %></div>
|
|
217
217
|
<% end %>
|
|
218
218
|
</div>
|
|
219
219
|
<% end %>
|
|
220
220
|
<div class="skills">
|
|
221
221
|
<span class="label">Skills</span>
|
|
222
|
-
<% dig(
|
|
222
|
+
<% experience.dig(:skills).each do |item| %>
|
|
223
223
|
<span class="skill-item">• <%= item %></span>
|
|
224
224
|
<% end %>
|
|
225
225
|
</div>
|
|
@@ -228,28 +228,28 @@
|
|
|
228
228
|
</div>
|
|
229
229
|
<% end %>
|
|
230
230
|
|
|
231
|
-
<% if
|
|
231
|
+
<% if data[:education] %>
|
|
232
232
|
<div class="education">
|
|
233
233
|
<h2 class="upcase heading">Education</h2>
|
|
234
|
-
<%
|
|
234
|
+
<% data[:education].each do |education| %>
|
|
235
235
|
<div class="education-item">
|
|
236
|
-
<h3><%= dig(
|
|
236
|
+
<h3><%= education.dig(:qualification) %></h3>
|
|
237
237
|
<div class="flex">
|
|
238
238
|
<div class="flex-grow">
|
|
239
239
|
<span style="font-weight: bold">
|
|
240
|
-
<%= dig(
|
|
240
|
+
<%= education.dig(:institute) %>
|
|
241
241
|
</span> /
|
|
242
|
-
<span><%= "#{dig(
|
|
242
|
+
<span><%= "#{education.dig(:location, :city)}, #{education.dig(:location, :country)}" %></span>
|
|
243
243
|
</div>
|
|
244
244
|
<div class="time-period">
|
|
245
245
|
<span class="time">
|
|
246
|
-
<%=
|
|
247
|
-
<%=
|
|
246
|
+
<%= education[:from] ? "#{month_name education.dig(:from, :month)} #{education.dig(:from, :year)} - " : '' %>
|
|
247
|
+
<%= education[:to] ? "#{month_name education.dig(:to, :month)} #{education.dig(:to, :year)}" : 'Present' %>
|
|
248
248
|
</span>
|
|
249
249
|
</div>
|
|
250
250
|
</div>
|
|
251
251
|
<div class="achievements-section">
|
|
252
|
-
<% dig(
|
|
252
|
+
<% education.dig(:achievements).each do |achievement| %>
|
|
253
253
|
<div>
|
|
254
254
|
• <%= achievement %>
|
|
255
255
|
</div>
|
|
@@ -260,4 +260,4 @@
|
|
|
260
260
|
</div>
|
|
261
261
|
<% end %>
|
|
262
262
|
</body>
|
|
263
|
-
</html>
|
|
263
|
+
</html>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kamisaku
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sinaru Gunawardena
|
|
@@ -33,16 +33,14 @@ files:
|
|
|
33
33
|
- kamisaku.png
|
|
34
34
|
- lib/kamisaku.rb
|
|
35
35
|
- lib/kamisaku/arg_parser.rb
|
|
36
|
-
- lib/kamisaku/
|
|
37
|
-
- lib/kamisaku/
|
|
38
|
-
- lib/kamisaku/
|
|
39
|
-
- lib/kamisaku/
|
|
40
|
-
- lib/kamisaku/
|
|
41
|
-
- lib/kamisaku/
|
|
42
|
-
- lib/kamisaku/cv_data_sections/skill.rb
|
|
43
|
-
- lib/kamisaku/cv_generator.rb
|
|
44
|
-
- lib/kamisaku/meta_file_parser.rb
|
|
36
|
+
- lib/kamisaku/chrome_runner.rb
|
|
37
|
+
- lib/kamisaku/cli_runner.rb
|
|
38
|
+
- lib/kamisaku/content_validator.rb
|
|
39
|
+
- lib/kamisaku/errors.rb
|
|
40
|
+
- lib/kamisaku/helpers.rb
|
|
41
|
+
- lib/kamisaku/html_builder.rb
|
|
45
42
|
- lib/kamisaku/pdf.rb
|
|
43
|
+
- lib/kamisaku/template_helpers.rb
|
|
46
44
|
- lib/kamisaku/version.rb
|
|
47
45
|
- lib/templates/paper/template.html.erb
|
|
48
46
|
- lib/templates/sleek/template.html.erb
|
data/lib/kamisaku/cv_data.rb
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Kamisaku
|
|
4
|
-
class CvData
|
|
5
|
-
DATA_SECTION_CLASSES = [
|
|
6
|
-
CvDataSection::Skill,
|
|
7
|
-
CvDataSection::Experience,
|
|
8
|
-
CvDataSection::Education,
|
|
9
|
-
CvDataSection::Interest
|
|
10
|
-
]
|
|
11
|
-
def initialize(hash)
|
|
12
|
-
@hash = hash
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def get_bindings
|
|
16
|
-
binding
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def has?(*path)
|
|
20
|
-
if DATA_SECTION_CLASSES.any? { |klass| path.first.instance_of? klass }
|
|
21
|
-
return path.first.has?(*path[1..])
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
path_s = path.map(&:to_s)
|
|
25
|
-
!@hash.dig(*path_s).nil?
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def dig(*path)
|
|
29
|
-
if DATA_SECTION_CLASSES.any? { |klass| path.first.instance_of? klass }
|
|
30
|
-
return path.first.dig(*path[1..])
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
path_s = path.map(&:to_s)
|
|
34
|
-
case path_s.first
|
|
35
|
-
when "skills"
|
|
36
|
-
@hash[path_s.first].each do |skill_hash|
|
|
37
|
-
yield CvDataSection::Skill.new(skill_hash)
|
|
38
|
-
end
|
|
39
|
-
when "experiences"
|
|
40
|
-
@hash[path_s.first].each do |skill_hash|
|
|
41
|
-
yield CvDataSection::Experience.new(skill_hash)
|
|
42
|
-
end
|
|
43
|
-
when "education"
|
|
44
|
-
@hash[path_s.first].each do |skill_hash|
|
|
45
|
-
yield CvDataSection::Education.new(skill_hash)
|
|
46
|
-
end
|
|
47
|
-
when "interests"
|
|
48
|
-
@hash[path_s.first].each do |skill_hash|
|
|
49
|
-
CvDataSection::Interest.new(skill_hash)
|
|
50
|
-
end
|
|
51
|
-
else
|
|
52
|
-
@hash.dig(*path_s)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module CvDataSection
|
|
4
|
-
class Base
|
|
5
|
-
def initialize(hash)
|
|
6
|
-
@hash = hash
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def get_bindings
|
|
10
|
-
binding
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def has?(*path)
|
|
14
|
-
path_s = path.map(&:to_s)
|
|
15
|
-
data = @hash.dig(*path_s)
|
|
16
|
-
!data.nil?
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def dig(*path)
|
|
20
|
-
path_s = path.map(&:to_s)
|
|
21
|
-
@hash.dig(*path_s)
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module CvDataSection
|
|
4
|
-
class Education < Base
|
|
5
|
-
include ParseHelper::Date
|
|
6
|
-
|
|
7
|
-
def dig(*path)
|
|
8
|
-
data = super
|
|
9
|
-
return [] if data.nil? && path.first == :achievements
|
|
10
|
-
return to_month(data) if path[-2..] == [:from, :month]
|
|
11
|
-
return to_month(data) if path[-2..] == [:to, :month]
|
|
12
|
-
data
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module CvDataSection
|
|
4
|
-
class Experience < Base
|
|
5
|
-
include ParseHelper::Date
|
|
6
|
-
def dig(*path)
|
|
7
|
-
data = super
|
|
8
|
-
return [] if data.nil? && path.first == :technologies
|
|
9
|
-
return [] if data.nil? && path.first == :achievements
|
|
10
|
-
return to_month(data) if path[-2..] == [:from, :month]
|
|
11
|
-
return to_month(data) if path[-2..] == [:to, :month]
|
|
12
|
-
data
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "erb"
|
|
4
|
-
|
|
5
|
-
module Kamisaku
|
|
6
|
-
class CvGenerator
|
|
7
|
-
attr_reader :pdf_location, :html_location, :cv_data, :template
|
|
8
|
-
|
|
9
|
-
def initialize(cv_data, pdf_location:, html_location: nil, template: nil)
|
|
10
|
-
@cv_data = cv_data
|
|
11
|
-
@pdf_location = pdf_location
|
|
12
|
-
@html_location = html_location
|
|
13
|
-
@template = template || "sleek"
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def generate
|
|
17
|
-
generated_html_file do |file_path|
|
|
18
|
-
FileUtils.cp(file_path, html_location) if html_location
|
|
19
|
-
html_file_to_pdf_file(file_path, pdf_location)
|
|
20
|
-
soft_remove_metadata_from_pdf_file(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 soft_remove_metadata_from_pdf_file(file_path)
|
|
39
|
-
command = <<~CMD
|
|
40
|
-
exiftool -all= #{file_path} -overwrite_original
|
|
41
|
-
CMD
|
|
42
|
-
system(command)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def generated_html_file
|
|
46
|
-
temp_html_file = Tempfile.new(%w[kamisaku .html])
|
|
47
|
-
temp_html_file.write(cv_html)
|
|
48
|
-
temp_html_file.close
|
|
49
|
-
begin
|
|
50
|
-
yield temp_html_file.path
|
|
51
|
-
ensure
|
|
52
|
-
temp_html_file.unlink
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def cv_html
|
|
57
|
-
rhtml = ERB.new(template_html)
|
|
58
|
-
rhtml.result(@cv_data.get_bindings)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def template_html
|
|
62
|
-
path = File.join(File.dirname(__FILE__), "/../templates/#{template}/template.html.erb")
|
|
63
|
-
File.read(path)
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
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
|