my-last-cv 0.0.5 → 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: 94c3e91fc809c66ce9bff0a797dbc4701562502617080a3dfc1980ff72d6d7a8
4
- data.tar.gz: 0d92a8897223468db3de303b7a23e312873cc2da76d3b0976e2f642e7863e442
3
+ metadata.gz: 56c56926e52415beebd4ff21594a85a4ef86be44f2ebe1716b5454204714f0e9
4
+ data.tar.gz: c75b9746a02372e488fd237f9d6ee667f9136b126826716b870c3f74816a0a1d
5
5
  SHA512:
6
- metadata.gz: cdfc99a9a7ae9d9145c643df109dedb8f9c02860e1d2d92c859a4d28a55a82bb8aff49e5d1fd478683b1e8d98f952d43d88a243749c12691f39a31489c000c4d
7
- data.tar.gz: b8970a939e5ff4f47640dc3021cb743e4794e58a96d41db4c74ce4ed75949cc4481fd274d7b9a708a2c3badb8d257fe5982308f7b3fa931b1329cfb9d260fb20
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,62 +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
- lignes = @markdown.lines.map(&:chomp)
9
- resultat = { sections: [] }
10
- section_actuelle = nil
11
- element_actuel = nil
12
-
13
- lignes.each do |ligne|
14
- line = ligne
15
- next if line.strip.empty?
16
-
17
- if (m = line.match(/^#\s+(.*)/))
18
- resultat[:name] = m[1].strip
19
- section_actuelle = nil
20
- element_actuel = nil
21
-
22
- elsif (m = line.match(/^(email|phone|location):\s*(.+)/i))
23
- (resultat[:contact] ||= []) << m[2].strip
24
-
25
- elsif (m = line.match(/^##\s+(.*)/))
26
- section_actuelle = { title: m[1].strip, items: [], elements: [] }
27
- resultat[:sections] << section_actuelle
28
- element_actuel = nil
29
-
30
- elsif (m = line.match(/^###\s+(.*)/))
31
- section_actuelle ||= { title: "Divers", items: [], elements: [] }
32
- resultat[:sections] << section_actuelle unless resultat[:sections].include?(section_actuelle)
33
-
34
- element_actuel = { title: m[1].strip, items: [] }
35
- section_actuelle[:elements] ||= []
36
- section_actuelle[:elements] << element_actuel
37
-
38
- elsif (m = line.match(/^[-*]\s+(.*)/))
39
- if element_actuel
40
- element_actuel[:items] << m[1].strip
41
- else
42
- section_actuelle ||= { title: "Divers", items: [], elements: [] }
43
- resultat[:sections] << section_actuelle unless resultat[:sections].include?(section_actuelle)
44
- section_actuelle[:items] << m[1].strip
45
- end
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)
46
33
  else
47
- if element_actuel && !element_actuel[:items].empty?
48
- element_actuel[:items][-1] += " " + line.strip
49
- elsif section_actuelle && !section_actuelle[:items].empty?
50
- section_actuelle[:items][-1] += " " + line.strip
51
- else
52
- section_actuelle ||= { title: "Divers", items: [], elements: [] }
53
- section_actuelle[:items] << line.strip
54
- end
34
+ handle_text(line)
35
+ end
36
+ end
37
+
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 = {}
68
+ end
69
+
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?
73
+
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
55
79
  end
56
80
  end
57
81
 
58
- resultat[:contact] = (resultat[:contact] || []).join(" · ")
59
- resultat
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)
107
+ else
108
+ flush_pending_nowhere
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
164
+
165
+ def handle_text(line)
166
+ @pending << line.strip
60
167
  end
61
168
  end
62
169
  end
@@ -1,5 +1,7 @@
1
1
  require 'prawn'
2
- require "fileutils"
2
+ require 'prawn/icon'
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,48 +51,71 @@ 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)
60
63
  with_color(pdf, @style.accent_color) do
61
- pdf.stroke_horizontal_rule
64
+ pdf.text(@parsed_cv[:title] || '-', size: @style.header_size, align: :center)
62
65
  end
63
- pdf.move_down 12
66
+
67
+ pdf.move_down 4
68
+ pdf.stroke_color(@style.accent_color)
69
+ pdf.stroke_horizontal_rule
70
+
71
+ if @parsed_cv[:intro]&.any?
72
+ pdf.move_down 10
73
+ @parsed_cv[:intro].each do |p|
74
+ pdf.text(p, size: @style.body_size, leading: 2)
75
+ end
76
+ end
77
+ pdf.move_down 10
64
78
  end
65
79
 
66
80
  def render_sections(pdf)
67
- @parsed_cv[:sections].each do |section|
81
+ (@parsed_cv[:sections] || []).each do |section|
68
82
  pdf.move_down 8
69
83
  pdf.font(@style.section_font)
70
- pdf.text(section[:title], size: @style.section_size, style: :bold)
71
84
 
72
- unless section[:items].nil? || section[:items].empty?
73
- pdf.move_down 4
74
- pdf.font(@style.body_font)
75
- section[:items].each do |item|
76
- pdf.text("• #{item}", size: @style.body_size, indent_paragraphs: 16)
77
- end
85
+ with_color(pdf, @style.accent_color) do
86
+ pdf.text(section[:title], size: @style.section_size, style: :bold)
78
87
  end
79
88
 
89
+ render_items(pdf, section[:items])
90
+
80
91
  (section[:elements] || []).each do |el|
81
92
  pdf.move_down 6
82
93
  pdf.font(@style.section_font)
83
94
  pdf.text(el[:title], size: (@style.section_size - 3), style: :bold)
84
-
85
- pdf.move_down 2
86
- pdf.font(@style.body_font)
87
- (el[:items] || []).each do |item|
88
- pdf.text("• #{item}", size: @style.body_size, indent_paragraphs: 20)
89
- end
95
+ render_items(pdf, el[:items])
90
96
  end
91
97
  end
92
98
  end
93
99
 
100
+ def render_items(pdf, items)
101
+ return if items.nil? || items.empty?
102
+
103
+ pdf.move_down 4
104
+ pdf.font(@style.body_font)
105
+
106
+ (items || []).each do |item|
107
+ case item[:type]
108
+ when :bullet
109
+ pdf.text("• #{item[:text]}", size: @style.body_size, indent_paragraphs: 16)
110
+ when :paragraph
111
+ pdf.text(item[:text], size: @style.body_size, leading: 2)
112
+ pdf.move_down 4
113
+ else
114
+ # fallback (au cas où)
115
+ pdf.text(item.to_s, size: @style.body_size)
116
+ end
117
+ end
118
+ end
94
119
 
95
120
  def with_color(pdf, hex)
96
121
  return yield if hex.to_s.strip.empty?
@@ -1,3 +1,3 @@
1
1
  module MyLastCV
2
- VERSION = "0.0.5"
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.5
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maxime Hanicotte
@@ -23,6 +23,50 @@ 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'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.3'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.3'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.13'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.13'
68
+ description: MyLastCV turns a Markdown CV into a styled PDF using Prawn. Configurable
69
+ styles (fonts, sizes, margins, accent color) and project-level custom fonts support.
26
70
  email:
27
71
  - max_hanicotte@msn.com
28
72
  executables:
@@ -33,6 +77,7 @@ files:
33
77
  - README.md
34
78
  - exe/my_last_cv
35
79
  - lib/my_last_cv.rb
80
+ - lib/my_last_cv/icons.rb
36
81
  - lib/my_last_cv/parser.rb
37
82
  - lib/my_last_cv/renderer.rb
38
83
  - lib/my_last_cv/style.rb
@@ -40,7 +85,13 @@ files:
40
85
  homepage: https://www.maxime.hanicotte.net/my-last-cv/
41
86
  licenses:
42
87
  - GPL-3.0-or-later
43
- metadata: {}
88
+ metadata:
89
+ homepage_uri: https://www.maxime.hanicotte.net/my-last-cv/
90
+ source_code_uri: https://github.com/maxime-hanicotte/my-last-cv
91
+ changelog_uri: https://github.com/maxime-hanicotte/my_last_cv/blob/main/CHANGELOG.md
92
+ bug_tracker_uri: https://github.com/maxime-hanicotte/my_last_cv/issues
93
+ documentation_uri: https://github.com/maxime-hanicotte/my_last_cv#readme
94
+ rubygems_mfa_required: 'true'
44
95
  rdoc_options: []
45
96
  require_paths:
46
97
  - lib