my-last-cv 0.0.6 → 0.0.7
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/lib/my_last_cv/icons.rb +14 -0
- data/lib/my_last_cv/parser.rb +148 -95
- data/lib/my_last_cv/renderer.rb +21 -15
- data/lib/my_last_cv/version.rb +1 -1
- metadata +16 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 56c56926e52415beebd4ff21594a85a4ef86be44f2ebe1716b5454204714f0e9
|
4
|
+
data.tar.gz: c75b9746a02372e488fd237f9d6ee667f9136b126826716b870c3f74816a0a1d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 229b38de93a8b39be1ba36ae5f3de149e3bdef877967ee0dd2d6c4cf8db14003de54f804438da98d4711d9f37c6249d92c0a8c3a17e190fdd66d227736c1af08
|
7
|
+
data.tar.gz: 9426974cfee0e9c7abbf5759bb95a449ecf210ab24bc7c57db04889a5480f8a27a2ff9c2ccd4dab7efb201a4f60089673e1b118890fa61e6ea9bc02a5f10aa42
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module MyLastCV
|
2
|
+
module Icons
|
3
|
+
MAP = {
|
4
|
+
name: "fas-user",
|
5
|
+
title: "fas-briefcase",
|
6
|
+
email: "fas-envelope",
|
7
|
+
phone: "fas-phone",
|
8
|
+
linkedin: "fab-linkedin",
|
9
|
+
github: "fab-github",
|
10
|
+
website: "fas-globe",
|
11
|
+
location: "fas-location-dot"
|
12
|
+
}.freeze
|
13
|
+
end
|
14
|
+
end
|
data/lib/my_last_cv/parser.rb
CHANGED
@@ -1,116 +1,169 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
1
3
|
module MyLastCV
|
2
4
|
class Parser
|
5
|
+
LineHandler = Struct.new(:pattern, :method)
|
6
|
+
|
7
|
+
HANDLERS = [
|
8
|
+
LineHandler.new(/^(name|title|email|phone|linkedin|github|website|location):\s*(.+)/i, :handle_contact),
|
9
|
+
LineHandler.new(/^#\s+(.*)/, :handle_title),
|
10
|
+
LineHandler.new(/^##\s+(.*)/, :handle_section),
|
11
|
+
LineHandler.new(/^###\s+(.*)/, :handle_element),
|
12
|
+
LineHandler.new(/^[-*]\s+(.*)/, :handle_bullet)
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
CONTACT_KEYS = %w[name title email phone linkedin github website location].freeze
|
16
|
+
|
3
17
|
def initialize(markdown)
|
4
18
|
@markdown = markdown
|
5
19
|
end
|
6
20
|
|
7
21
|
def parse
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
return if text.empty?
|
23
|
-
target[:items] ||= []
|
24
|
-
target[:items] << { type: :paragraph, text: text }
|
22
|
+
setup_state
|
23
|
+
extract_front_matter!
|
24
|
+
@markdown.each_line do |raw|
|
25
|
+
line = raw.rstrip
|
26
|
+
|
27
|
+
stripped = line.strip
|
28
|
+
next if stripped.empty? || stripped == '---'
|
29
|
+
|
30
|
+
handler = HANDLERS.find { |h| h.pattern.match?(line) }
|
31
|
+
if handler
|
32
|
+
send(handler.method, line)
|
33
|
+
else
|
34
|
+
handle_text(line)
|
35
|
+
end
|
25
36
|
end
|
26
37
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
38
|
+
finalize
|
39
|
+
@result
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def setup_state
|
45
|
+
@result = { sections: [] }
|
46
|
+
@current_section = nil
|
47
|
+
@current_element = nil
|
48
|
+
@pending = []
|
49
|
+
end
|
50
|
+
|
51
|
+
def extract_front_matter!
|
52
|
+
lines = @markdown.lines
|
53
|
+
return if lines.empty?
|
54
|
+
return unless lines.first&.strip == '---'
|
55
|
+
|
56
|
+
closing_index = lines[1..]&.find_index { |l| l.strip == '---' }
|
57
|
+
return unless closing_index
|
58
|
+
|
59
|
+
closing_index += 1
|
60
|
+
|
61
|
+
yaml_content = lines[1...closing_index].join
|
62
|
+
rest = lines[(closing_index + 1)..] || []
|
63
|
+
|
64
|
+
begin
|
65
|
+
data = YAML.safe_load(yaml_content) || {}
|
66
|
+
rescue StandardError
|
67
|
+
data = {}
|
34
68
|
end
|
35
69
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
if (m = line.match(/^#\s+(.*)/)) # New header
|
41
|
-
if current_element
|
42
|
-
flush_pending_to.call(current_element)
|
43
|
-
elsif current_section
|
44
|
-
flush_pending_to.call(current_section)
|
45
|
-
else
|
46
|
-
flush_pending_nowhere.call
|
47
|
-
end
|
48
|
-
|
49
|
-
result[:name] = m[1].strip
|
50
|
-
current_section = nil
|
51
|
-
current_element = nil
|
52
|
-
|
53
|
-
elsif (m = line.match(/^(email|phone|location):\s*(.+)/i))
|
54
|
-
(result[:contact] ||= []) << m[2].strip
|
55
|
-
|
56
|
-
elsif (m = line.match(/^##\s+(.*)/)) # New section
|
57
|
-
if current_element
|
58
|
-
flush_pending_to.call(current_element)
|
59
|
-
elsif current_section
|
60
|
-
flush_pending_to.call(current_section)
|
61
|
-
else
|
62
|
-
flush_pending_nowhere.call
|
63
|
-
end
|
64
|
-
|
65
|
-
current_section = { title: m[1].strip, items: [], elements: [] }
|
66
|
-
result[:sections] << current_section
|
67
|
-
current_element = nil
|
68
|
-
|
69
|
-
elsif (m = line.match(/^###\s+(.*)/)) # New element
|
70
|
-
if current_element
|
71
|
-
flush_pending_to.call(current_element)
|
72
|
-
elsif current_section
|
73
|
-
flush_pending_to.call(current_section)
|
74
|
-
else
|
75
|
-
flush_pending_nowhere.call
|
76
|
-
end
|
77
|
-
|
78
|
-
current_section ||= { title: "Divers", items: [], elements: [] }
|
79
|
-
result[:sections] << current_section unless result[:sections].include?(current_section)
|
80
|
-
|
81
|
-
current_element = { title: m[1].strip, items: [] }
|
82
|
-
current_section[:elements] ||= []
|
83
|
-
current_section[:elements] << current_element
|
84
|
-
|
85
|
-
elsif (m = line.match(/^[-*]\s+(.*)/)) # Bullet point
|
86
|
-
item = { type: :bullet, text: m[1].strip }
|
87
|
-
if current_element
|
88
|
-
flush_pending_to.call(current_element)
|
89
|
-
current_element[:items] << item
|
90
|
-
elsif current_section
|
91
|
-
flush_pending_to.call(current_section)
|
92
|
-
current_section[:items] << item
|
93
|
-
else
|
94
|
-
current_section = { title: "Divers", items: [item], elements: [] }
|
95
|
-
result[:sections] << current_section
|
96
|
-
end
|
70
|
+
if data.is_a?(Hash)
|
71
|
+
# Prefer explicit YAML title if present
|
72
|
+
@result[:title] = data['title'].to_s.strip unless data['title'].to_s.strip.empty?
|
97
73
|
|
98
|
-
|
99
|
-
|
74
|
+
CONTACT_KEYS.each do |k|
|
75
|
+
v = data[k]
|
76
|
+
next if v.nil? || v.to_s.strip.empty?
|
77
|
+
@result[:contact] ||= {}
|
78
|
+
@result[:contact][k] = v.to_s.strip
|
100
79
|
end
|
101
80
|
end
|
102
81
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
82
|
+
@markdown = rest.join
|
83
|
+
end
|
84
|
+
|
85
|
+
def flush_pending_to(target)
|
86
|
+
return if @pending.empty?
|
87
|
+
text = @pending.join(" ").strip
|
88
|
+
@pending.clear
|
89
|
+
return if text.empty?
|
90
|
+
target[:items] ||= []
|
91
|
+
target[:items] << { type: :paragraph, text: text }
|
92
|
+
end
|
93
|
+
|
94
|
+
def flush_pending_nowhere
|
95
|
+
return if @pending.empty?
|
96
|
+
text = @pending.join(" ").strip
|
97
|
+
@pending.clear
|
98
|
+
return if text.empty?
|
99
|
+
(@result[:intro] ||= []) << text
|
100
|
+
end
|
101
|
+
|
102
|
+
def finalize
|
103
|
+
if @current_element
|
104
|
+
flush_pending_to(@current_element)
|
105
|
+
elsif @current_section
|
106
|
+
flush_pending_to(@current_section)
|
108
107
|
else
|
109
|
-
flush_pending_nowhere
|
108
|
+
flush_pending_nowhere
|
110
109
|
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def flush_pending_current
|
113
|
+
if @current_element
|
114
|
+
flush_pending_to(@current_element)
|
115
|
+
elsif @current_section
|
116
|
+
flush_pending_to(@current_section)
|
117
|
+
else
|
118
|
+
flush_pending_nowhere
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def handle_title(line)
|
123
|
+
flush_pending_current
|
124
|
+
@result[:title] = line.sub(/^#\s+/, "").strip
|
125
|
+
@current_section = nil
|
126
|
+
@current_element = nil
|
127
|
+
end
|
128
|
+
|
129
|
+
def handle_contact(line)
|
130
|
+
key, value = line.split(":", 2)
|
131
|
+
@result[:contact] ||= {}
|
132
|
+
@result[:contact][key] = value.strip
|
133
|
+
end
|
134
|
+
|
135
|
+
def handle_section(line)
|
136
|
+
flush_pending_current
|
137
|
+
@current_section = { title: line.sub(/^##\s+/, "").strip, items: [], elements: [] }
|
138
|
+
@result[:sections] << @current_section
|
139
|
+
@current_element = nil
|
140
|
+
end
|
141
|
+
|
142
|
+
def handle_element(line)
|
143
|
+
flush_pending_current
|
144
|
+
@current_section ||= { title: "Divers", items: [], elements: [] }
|
145
|
+
@result[:sections] << @current_section unless @result[:sections].include?(@current_section)
|
146
|
+
@current_element = { title: line.sub(/^###\s+/, "").strip, items: [] }
|
147
|
+
@current_section[:elements] ||= []
|
148
|
+
@current_section[:elements] << @current_element
|
149
|
+
end
|
150
|
+
|
151
|
+
def handle_bullet(line)
|
152
|
+
item = { type: :bullet, text: line.sub(/^[-*]\s+/, "").strip }
|
153
|
+
if @current_element
|
154
|
+
flush_pending_to(@current_element)
|
155
|
+
@current_element[:items] << item
|
156
|
+
elsif @current_section
|
157
|
+
flush_pending_to(@current_section)
|
158
|
+
@current_section[:items] << item
|
159
|
+
else
|
160
|
+
@current_section = { title: "Divers", items: [item], elements: [] }
|
161
|
+
@result[:sections] << @current_section
|
162
|
+
end
|
163
|
+
end
|
111
164
|
|
112
|
-
|
113
|
-
|
165
|
+
def handle_text(line)
|
166
|
+
@pending << line.strip
|
114
167
|
end
|
115
168
|
end
|
116
169
|
end
|
data/lib/my_last_cv/renderer.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'prawn'
|
2
|
+
require 'prawn/icon'
|
2
3
|
require 'fileutils'
|
4
|
+
require_relative 'icons'
|
3
5
|
|
4
6
|
module MyLastCV
|
5
7
|
class Renderer
|
@@ -35,7 +37,7 @@ module MyLastCV
|
|
35
37
|
fonts_dir = resolved_fonts_dir
|
36
38
|
return unless fonts_dir
|
37
39
|
|
38
|
-
#
|
40
|
+
# Example : Inter (Regular / Bold)
|
39
41
|
pdf.font_families.update(
|
40
42
|
"Inter" => {
|
41
43
|
normal: File.join(fonts_dir, "Inter-Regular.ttf"),
|
@@ -49,36 +51,40 @@ module MyLastCV
|
|
49
51
|
end
|
50
52
|
|
51
53
|
def render_header(pdf)
|
52
|
-
pdf.font(@style.header_font)
|
53
54
|
pdf.move_down 12
|
54
|
-
|
55
|
-
|
55
|
+
pdf.font(@style.body_font)
|
56
|
+
@parsed_cv[:contact].each_key do |key|
|
57
|
+
icon = "<icon size=\"#{@style.body_size}\">#{MyLastCV::Icons::MAP[key.to_sym]}</icon>"
|
58
|
+
pdf.icon("#{icon} #{@parsed_cv[:contact][key]}", inline_format: true)
|
56
59
|
end
|
60
|
+
|
57
61
|
pdf.move_down 6
|
58
|
-
pdf.font(@style.
|
59
|
-
pdf
|
62
|
+
pdf.font(@style.header_font)
|
63
|
+
with_color(pdf, @style.accent_color) do
|
64
|
+
pdf.text(@parsed_cv[:title] || '-', size: @style.header_size, align: :center)
|
65
|
+
end
|
66
|
+
|
67
|
+
pdf.move_down 4
|
68
|
+
pdf.stroke_color(@style.accent_color)
|
69
|
+
pdf.stroke_horizontal_rule
|
60
70
|
|
61
71
|
if @parsed_cv[:intro]&.any?
|
62
72
|
pdf.move_down 10
|
63
73
|
@parsed_cv[:intro].each do |p|
|
64
74
|
pdf.text(p, size: @style.body_size, leading: 2)
|
65
|
-
pdf.move_down 4
|
66
75
|
end
|
67
|
-
else
|
68
|
-
pdf.move_down 4
|
69
|
-
end
|
70
|
-
|
71
|
-
with_color(pdf, @style.accent_color) do
|
72
|
-
pdf.stroke_horizontal_rule
|
73
76
|
end
|
74
|
-
pdf.move_down
|
77
|
+
pdf.move_down 10
|
75
78
|
end
|
76
79
|
|
77
80
|
def render_sections(pdf)
|
78
81
|
(@parsed_cv[:sections] || []).each do |section|
|
79
82
|
pdf.move_down 8
|
80
83
|
pdf.font(@style.section_font)
|
81
|
-
|
84
|
+
|
85
|
+
with_color(pdf, @style.accent_color) do
|
86
|
+
pdf.text(section[:title], size: @style.section_size, style: :bold)
|
87
|
+
end
|
82
88
|
|
83
89
|
render_items(pdf, section[:items])
|
84
90
|
|
data/lib/my_last_cv/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: my-last-cv
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maxime Hanicotte
|
@@ -23,6 +23,20 @@ dependencies:
|
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: '2.5'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: prawn-icon
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '4.1'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '4.1'
|
26
40
|
- !ruby/object:Gem::Dependency
|
27
41
|
name: rake
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
@@ -63,6 +77,7 @@ files:
|
|
63
77
|
- README.md
|
64
78
|
- exe/my_last_cv
|
65
79
|
- lib/my_last_cv.rb
|
80
|
+
- lib/my_last_cv/icons.rb
|
66
81
|
- lib/my_last_cv/parser.rb
|
67
82
|
- lib/my_last_cv/renderer.rb
|
68
83
|
- lib/my_last_cv/style.rb
|