my-last-cv 0.0.7 → 0.0.8

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: 56c56926e52415beebd4ff21594a85a4ef86be44f2ebe1716b5454204714f0e9
4
- data.tar.gz: c75b9746a02372e488fd237f9d6ee667f9136b126826716b870c3f74816a0a1d
3
+ metadata.gz: 031a4dc86e72946d0fb8673b77738a6cbb3d30c2404c028267a88a78b8ae1463
4
+ data.tar.gz: b2ac4623552d0ae8f597f8dd58cb9bcb481b09f201ff899335054767cfcd1620
5
5
  SHA512:
6
- metadata.gz: 229b38de93a8b39be1ba36ae5f3de149e3bdef877967ee0dd2d6c4cf8db14003de54f804438da98d4711d9f37c6249d92c0a8c3a17e190fdd66d227736c1af08
7
- data.tar.gz: 9426974cfee0e9c7abbf5759bb95a449ecf210ab24bc7c57db04889a5480f8a27a2ff9c2ccd4dab7efb201a4f60089673e1b118890fa61e6ea9bc02a5f10aa42
6
+ metadata.gz: bcd26c3ab542f8b2ddf18dc894351bab8c1332181609164b47e2b79ef433d83ec6298509844f20907ee91caf1a6aceea43f451e76567dd48afc457fedd74f889
7
+ data.tar.gz: d38f99b4d9fab6a4c700628282ef21cc125590d8e4a4a1ab48ad625c5ebbed132cc1c89cc1b1b5cbf306c13bf503d4fa780d0c637c425388d8c0728345706c0f
data/README.md CHANGED
@@ -1,14 +1,123 @@
1
1
  # my-last-cv
2
- A simple gem to generate beautiful CV from a markdown file
3
2
 
4
- ## How to use it?
3
+ A Ruby gem and command-line tool that generates a styled PDF CV from Markdown.
4
+
5
+ ## Requirements
6
+
7
+ Ruby 3.0 or later.
8
+
9
+ ## Installation
10
+
5
11
  ```bash
6
12
  bundle add my-last-cv
7
13
  bundle install
8
- bundle exec my_last_cv sample/cv.md output/cv.pdf
9
14
  ```
10
15
 
16
+ For local development, install the project's dependencies:
17
+
18
+ ```bash
19
+ bundle install
20
+ ```
21
+
22
+ ## Generate a CV
23
+
24
+ ```bash
25
+ bundle exec my_last_cv path/to/cv.md output/cv.pdf
26
+ ```
27
+
28
+ The output directory is created automatically. The CLI uses the bundled Inter and
29
+ Lora fonts. Start from [`sample/cv.md`](sample/cv.md) to create your document.
30
+
31
+ ## Markdown format
32
+
33
+ Use YAML front matter for contact information, a level-one heading for the name,
34
+ level-two headings for sections, and level-three headings for entries within a
35
+ section:
36
+
37
+ ```markdown
38
+ ---
39
+ title: "Ruby developer"
40
+ email: "jane@example.com"
41
+ phone: "+33 6 12 34 56 78"
42
+ linkedin: "https://www.linkedin.com/in/janedoe"
43
+ github: "https://github.com/janedoe"
44
+ website: "https://janedoe.dev"
45
+ location: "Paris, France"
46
+ ---
47
+
48
+ # Jane Doe
49
+ Short professional introduction displayed below the header.
50
+
51
+ ## Experience
52
+ ### Senior Developer — Example Corp
53
+ Introductory paragraph for this role.
54
+ - Built and maintained Ruby applications.
55
+ - Mentored other developers.
56
+
57
+ ## Skills
58
+ - Ruby
59
+ - PostgreSQL
60
+ ```
61
+
62
+ Supported front-matter contact keys are `name`, `title`, `email`, `phone`,
63
+ `linkedin`, `github`, `website`, and `location`. They are rendered with icons.
64
+
65
+ Text immediately below the main heading becomes the introduction. Section and
66
+ entry content can contain paragraphs and bullet lists. A `###` entry without a
67
+ preceding `##` section is placed in a section named `Divers`.
68
+
69
+ ## Inline Markdown support
70
+
71
+ Contact fields, paragraphs, and bullet items support:
72
+
73
+ - `**bold**`
74
+ - `*italic*`
75
+ - `` `code` ``
76
+ - `[links](https://example.com)`
77
+
78
+ Escape Markdown punctuation with a backslash when it should remain literal, for
79
+ example `\*not italic\*`.
80
+
81
+ ## Ruby API and styling
82
+
83
+ ```ruby
84
+ require "my_last_cv"
85
+
86
+ style = MyLastCV::Style.new(
87
+ header_font: "Inter",
88
+ header_size: 22,
89
+ section_font: "Inter",
90
+ section_size: 12,
91
+ body_font: "Lora",
92
+ body_size: 10,
93
+ page_options: { margin: 36 },
94
+ accent_color: "1F7AE0"
95
+ )
96
+
97
+ MyLastCV.generate("cv.md", "output/cv.pdf", style: style)
98
+ ```
99
+
100
+ `MyLastCV::Style` defaults to Helvetica, sizes `18`, `12`, and `10`, a page
101
+ margin of `48`, and a black accent color (`000000`). `page_options` is passed to
102
+ `Prawn::Document`, so Prawn page settings such as `page_size` and `layout` can
103
+ also be supplied.
104
+
11
105
  ## Custom fonts
12
- By default, the gem looks for fonts in `./fonts` of the calling project.
13
- You can override this with `Style.new(fonts_dir: "/path/to/fonts")`
14
- or `MY_LAST_CV_FONTS_DIR=/path/to/fonts`.
106
+
107
+ Inter and Lora are bundled with the gem. To override them, provide a directory
108
+ containing `Inter-Regular.ttf`, `Inter-Bold.ttf`, `Lora-Regular.ttf`, and
109
+ `Lora-Bold.ttf`:
110
+
111
+ ```ruby
112
+ MyLastCV::Style.new(fonts_dir: "/path/to/fonts")
113
+ ```
114
+
115
+ Alternatively, set `MY_LAST_CV_FONTS_DIR=/path/to/fonts`. Font directories are
116
+ resolved in this order: `fonts_dir`, `MY_LAST_CV_FONTS_DIR`, `./fonts`, then the
117
+ fonts bundled with the gem.
118
+
119
+ ## Development
120
+
121
+ ```bash
122
+ bundle exec rake spec
123
+ ```
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,141 @@
1
+ require 'cgi'
2
+
3
+ module MyLastCV
4
+ class Inline
5
+ LINK_COLOR = '0563C1'.freeze
6
+ ESCAPABLE_CHARACTERS = "`*[]()".chars.freeze
7
+
8
+ def self.parse(text)
9
+ new(text).parse
10
+ end
11
+
12
+ def initialize(text)
13
+ @text = text.to_s
14
+ @index = 0
15
+ end
16
+
17
+ def parse
18
+ parse_until(nil).first
19
+ end
20
+
21
+ private
22
+
23
+ def parse_until(stop_token)
24
+ output = +""
25
+
26
+ while @index < @text.length
27
+ if stop_token && token?(stop_token)
28
+ @index += stop_token.length
29
+ return [output, true]
30
+ end
31
+
32
+ output << if escaped_character?
33
+ consume_escaped_character
34
+ elsif token?('**')
35
+ parse_emphasis('**', 'b')
36
+ elsif token?('*')
37
+ parse_emphasis('*', 'i')
38
+ elsif token?('`')
39
+ parse_code
40
+ elsif token?('[')
41
+ parse_link
42
+ else
43
+ consume_plain_character
44
+ end
45
+ end
46
+
47
+ [output, false]
48
+ end
49
+
50
+ def escaped_character?
51
+ current_character == '\\' && (next_character == '\\' || ESCAPABLE_CHARACTERS.include?(next_character))
52
+ end
53
+
54
+ def consume_escaped_character
55
+ character = next_character
56
+ @index += 2
57
+ CGI.escapeHTML(character)
58
+ end
59
+
60
+ def consume_plain_character
61
+ character = current_character
62
+ @index += 1
63
+ CGI.escapeHTML(character)
64
+ end
65
+
66
+ def parse_emphasis(delimiter, tag)
67
+ start_index = @index
68
+ @index += delimiter.length
69
+ content, closed = parse_until(delimiter)
70
+
71
+ return "<#{tag}>#{content}</#{tag}>" if closed && !content.empty?
72
+
73
+ CGI.escapeHTML(@text[start_index...@index])
74
+ end
75
+
76
+ def parse_code
77
+ start_index = @index
78
+ @index += 1
79
+ content = +""
80
+
81
+ while @index < @text.length && !token?('`')
82
+ content << @text[@index]
83
+ @index += 1
84
+ end
85
+
86
+ return CGI.escapeHTML(@text[start_index...@index]) if @index >= @text.length
87
+
88
+ @index += 1
89
+ "<font name=\"Courier\">#{CGI.escapeHTML(content)}</font>"
90
+ end
91
+
92
+ def parse_link
93
+ start_index = @index
94
+ @index += 1
95
+ label, closed = parse_until(']')
96
+ return CGI.escapeHTML(@text[start_index...@index]) unless closed
97
+ return CGI.escapeHTML(@text[start_index...@index]) unless token?('(')
98
+
99
+ @index += 1
100
+ url, closed = parse_link_destination
101
+ return CGI.escapeHTML(@text[start_index...@index]) unless closed
102
+ return CGI.escapeHTML(@text[start_index...@index]) if url.strip.empty?
103
+
104
+ %(<color rgb="#{LINK_COLOR}"><u><link href="#{CGI.escapeHTML(url)}">#{label}</link></u></color>)
105
+ end
106
+
107
+ def parse_link_destination
108
+ start_index = @index
109
+ depth = 0
110
+
111
+ while @index < @text.length
112
+ if token?('(')
113
+ depth += 1
114
+ elsif token?(')')
115
+ if depth.zero?
116
+ url = @text[start_index...@index]
117
+ @index += 1
118
+ return [url, true]
119
+ end
120
+
121
+ depth -= 1
122
+ end
123
+ @index += 1
124
+ end
125
+
126
+ [@text[start_index...@index], false]
127
+ end
128
+
129
+ def token?(value)
130
+ @text[@index, value.length] == value
131
+ end
132
+
133
+ def current_character
134
+ @text[@index]
135
+ end
136
+
137
+ def next_character
138
+ @text[@index + 1]
139
+ end
140
+ end
141
+ end
@@ -129,7 +129,8 @@ module MyLastCV
129
129
  def handle_contact(line)
130
130
  key, value = line.split(":", 2)
131
131
  @result[:contact] ||= {}
132
- @result[:contact][key] = value.strip
132
+ normalized_key = key.to_s.strip.downcase
133
+ @result[:contact][normalized_key] = value.to_s.strip
133
134
  end
134
135
 
135
136
  def handle_section(line)
@@ -2,6 +2,7 @@ require 'prawn'
2
2
  require 'prawn/icon'
3
3
  require 'fileutils'
4
4
  require_relative 'icons'
5
+ require_relative 'inline'
5
6
 
6
7
  module MyLastCV
7
8
  class Renderer
@@ -53,9 +54,10 @@ module MyLastCV
53
54
  def render_header(pdf)
54
55
  pdf.move_down 12
55
56
  pdf.font(@style.body_font)
56
- @parsed_cv[:contact].each_key do |key|
57
+ contact = @parsed_cv[:contact] || {}
58
+ contact.each_key do |key|
57
59
  icon = "<icon size=\"#{@style.body_size}\">#{MyLastCV::Icons::MAP[key.to_sym]}</icon>"
58
- pdf.icon("#{icon} #{@parsed_cv[:contact][key]}", inline_format: true)
60
+ pdf.icon("#{icon} #{Inline.parse(contact[key])}", inline_format: true)
59
61
  end
60
62
 
61
63
  pdf.move_down 6
@@ -71,7 +73,7 @@ module MyLastCV
71
73
  if @parsed_cv[:intro]&.any?
72
74
  pdf.move_down 10
73
75
  @parsed_cv[:intro].each do |p|
74
- pdf.text(p, size: @style.body_size, leading: 2)
76
+ pdf.text(Inline.parse(p), size: @style.body_size, leading: 2, inline_format: true)
75
77
  end
76
78
  end
77
79
  pdf.move_down 10
@@ -106,9 +108,9 @@ module MyLastCV
106
108
  (items || []).each do |item|
107
109
  case item[:type]
108
110
  when :bullet
109
- pdf.text("• #{item[:text]}", size: @style.body_size, indent_paragraphs: 16)
111
+ pdf.text("• #{Inline.parse(item[:text])}", size: @style.body_size, indent_paragraphs: 16, inline_format: true)
110
112
  when :paragraph
111
- pdf.text(item[:text], size: @style.body_size, leading: 2)
113
+ pdf.text(Inline.parse(item[:text]), size: @style.body_size, leading: 2, inline_format: true)
112
114
  pdf.move_down 4
113
115
  else
114
116
  # fallback (au cas où)
@@ -1,3 +1,3 @@
1
1
  module MyLastCV
2
- VERSION = "0.0.7"
2
+ VERSION = "0.0.8"
3
3
  end
data/lib/my_last_cv.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require_relative 'my_last_cv/version'
2
+ require_relative 'my_last_cv/inline'
2
3
  require_relative 'my_last_cv/parser'
3
4
  require_relative 'my_last_cv/style'
4
5
  require_relative 'my_last_cv/renderer'
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.7
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maxime Hanicotte
@@ -76,8 +76,13 @@ extra_rdoc_files: []
76
76
  files:
77
77
  - README.md
78
78
  - exe/my_last_cv
79
+ - fonts/Inter-Bold.ttf
80
+ - fonts/Inter-Regular.ttf
81
+ - fonts/Lora-Bold.ttf
82
+ - fonts/Lora-Regular.ttf
79
83
  - lib/my_last_cv.rb
80
84
  - lib/my_last_cv/icons.rb
85
+ - lib/my_last_cv/inline.rb
81
86
  - lib/my_last_cv/parser.rb
82
87
  - lib/my_last_cv/renderer.rb
83
88
  - lib/my_last_cv/style.rb
@@ -106,7 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
111
  - !ruby/object:Gem::Version
107
112
  version: '0'
108
113
  requirements: []
109
- rubygems_version: 3.7.1
114
+ rubygems_version: 3.7.2
110
115
  specification_version: 4
111
116
  summary: Generate your CV from Markdown to PDF
112
117
  test_files: []