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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d2bc19ff6ce026645ff9fc68e65345b76a3087db661be1eac66a48379b3395e
4
- data.tar.gz: 9edb1b606e386db51aa0272ea2ed87fe290c6273619fe5b65213430aa117663e
3
+ metadata.gz: 56c56926e52415beebd4ff21594a85a4ef86be44f2ebe1716b5454204714f0e9
4
+ data.tar.gz: c75b9746a02372e488fd237f9d6ee667f9136b126826716b870c3f74816a0a1d
5
5
  SHA512:
6
- metadata.gz: e9f25db116d350985c4a016bfea94876b926702c606e93faf3e8daf239e1b027f89654fddc0d3495cc10746a1c723f98057b848058e2a32dbefe02a6e13ee8d6
7
- data.tar.gz: f2802143d81a0d3a0a2fb9e8bd46f550bf093db531e9e43172d1d0746d24713251a90bdeed255034e6593e951c2c64993f5214ae14c0bb7456b27b6d14c73582
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
@@ -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
- lines = @markdown.lines.map(&:chomp)
9
- result = { sections: [] }
10
-
11
- current_section = nil
12
- current_element = nil
13
-
14
- # buffer
15
- pending = []
16
-
17
- # helpers
18
- flush_pending_to = lambda do |target|
19
- return if pending.empty?
20
- text = pending.join(" ").strip
21
- pending.clear
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
- flush_pending_nowhere = lambda do
28
- return if pending.empty?
29
- text = pending.join(" ").strip
30
- pending.clear
31
- return if text.empty?
32
- result[:intro] ||= []
33
- result[:intro] << text
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
- lines.each do |raw|
37
- line = raw.rstrip
38
- next if line.strip.empty?
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
- else
99
- pending << line.strip
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
- # End of parsing, flush any remaining pending text
104
- if current_element
105
- flush_pending_to.call(current_element)
106
- elsif current_section
107
- flush_pending_to.call(current_section)
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.call
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
- result[:contact] = (result[:contact] || []).join(" · ")
113
- result
165
+ def handle_text(line)
166
+ @pending << line.strip
114
167
  end
115
168
  end
116
169
  end
@@ -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
- # Exemple : Inter (Regular / Bold)
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
- with_color(pdf, @style.accent_color) do
55
- pdf.text(@parsed_cv[:name] || '-', size: @style.header_size, align: :center)
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.body_font)
59
- pdf.text(@parsed_cv[:contact] || '', size: @style.body_size, align: :center)
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 12
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
- pdf.text(section[:title], size: @style.section_size, style: :bold)
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
 
@@ -1,3 +1,3 @@
1
1
  module MyLastCV
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.7"
3
3
  end
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.6
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