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 +4 -4
- data/README.md +115 -6
- data/fonts/Inter-Bold.ttf +0 -0
- data/fonts/Inter-Regular.ttf +0 -0
- data/fonts/Lora-Bold.ttf +0 -0
- data/fonts/Lora-Regular.ttf +0 -0
- data/lib/my_last_cv/inline.rb +141 -0
- data/lib/my_last_cv/parser.rb +2 -1
- data/lib/my_last_cv/renderer.rb +7 -5
- data/lib/my_last_cv/version.rb +1 -1
- data/lib/my_last_cv.rb +1 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 031a4dc86e72946d0fb8673b77738a6cbb3d30c2404c028267a88a78b8ae1463
|
|
4
|
+
data.tar.gz: b2ac4623552d0ae8f597f8dd58cb9bcb481b09f201ff899335054767cfcd1620
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
data/fonts/Lora-Bold.ttf
ADDED
|
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
|
data/lib/my_last_cv/parser.rb
CHANGED
|
@@ -129,7 +129,8 @@ module MyLastCV
|
|
|
129
129
|
def handle_contact(line)
|
|
130
130
|
key, value = line.split(":", 2)
|
|
131
131
|
@result[:contact] ||= {}
|
|
132
|
-
|
|
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)
|
data/lib/my_last_cv/renderer.rb
CHANGED
|
@@ -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]
|
|
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} #{
|
|
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ù)
|
data/lib/my_last_cv/version.rb
CHANGED
data/lib/my_last_cv.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.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.
|
|
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: []
|