resme 0.3.1 → 0.5.0

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: 7b346f37deb079117dd466833d47d6b1f9d4b01e916c19c5b853652297fd3d67
4
- data.tar.gz: 0dfceeaabf0074a619d9f67faf499882ad641128e918b995cb8eb0d7a3b6fde2
3
+ metadata.gz: 27d07b29ff7369747e8f43ad9cbdc29efe1023cca20de554f94c02fdc33c57f8
4
+ data.tar.gz: fc048d5417183cba079e387caa3c08d2540fb937a384aae05759622b91b82846
5
5
  SHA512:
6
- metadata.gz: 2d7013a8e980341a3d3abdbb3d3f814df116b98915711257c8a79ca5b0cac1406ec4f158629c1190af332200667afffc1f8c8e88caa5f521bb63578a64416750
7
- data.tar.gz: d57c6fd89c9acf4ead44af434ed4ccba9b0c0117e1e1a653f6e0ba15307d1572d384550efa606c529516c37f1c548e77524a292573c2830aba6391ff1e2490e2
6
+ metadata.gz: 77bab2cbc35e21bb7ce669100d6b0f66d4b9f60e496814b35aec3a1fb3346730bbfe0700d884cde697500a479007cbac61dc834dcd358b3b1ab4375e59ee60db
7
+ data.tar.gz: fa2c84c4358a0c86469f83f11cb8c361a8a90602ed58eadf8cf53701757d4525d1f27f27599fdf361af46a2fcce0243c2e014395375c84a82489834be5b56118
data/CHANGELOG.org ADDED
@@ -0,0 +1,19 @@
1
+ #+TITLE: CHANGELOG
2
+ #+AUTHOR: Adolfo Villafiorita
3
+
4
+ * Version 1.5.0
5
+ - [user] New command =view= allows to view the template used
6
+ for generating a resume in a specific format
7
+ - [user] Check command is now based on ClassyHash
8
+ - [code] Various changes to the code
9
+
10
+ * Version 1.4.0
11
+ - [user] New option =--skip= allows to skip some top-level sections.
12
+ You mileage might vary, as some formats might require the
13
+ information you are trying to skip
14
+ - [user] New command =generate= is now used to generate the Resume
15
+ in different formats.
16
+ - [user] New option =--erb= allows to specify a custom template for
17
+ generating the resume. Use it in conjunction with =view= (released
18
+ in version 1.5.0) to jump-start your layout.
19
+
data/README.org ADDED
@@ -0,0 +1,272 @@
1
+ #+TITLE: RESME - A Resume Generator
2
+ #+AUTHOR: Adolfo Villafiorita
3
+ #+DATE: <2020-07-14 Tue>
4
+ #+STARTUP: showall
5
+
6
+ Keep your resume in YAML and output it in various formats, including
7
+ Org Mode, Markdown, JSON, and the Europass XML format.
8
+
9
+ The rendering engine is based on ERB. This simplifies the creation of
10
+ new output formats (and extending/modifying the YML structure to one's
11
+ needs).
12
+
13
+ ** Installation
14
+ :PROPERTIES:
15
+ :CUSTOM_ID: installation
16
+ :END:
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ #+BEGIN_SRC ruby
21
+ gem 'resme'
22
+ #+END_SRC
23
+
24
+ And then execute:
25
+
26
+ #+BEGIN_EXAMPLE
27
+ $ bundle
28
+ #+END_EXAMPLE
29
+
30
+ Or install it yourself as:
31
+
32
+ #+BEGIN_EXAMPLE
33
+ $ gem install resme
34
+ #+END_EXAMPLE
35
+
36
+ ** Usage
37
+ :PROPERTIES:
38
+ :CUSTOM_ID: usage
39
+ :END:
40
+
41
+ Start with:
42
+
43
+ #+BEGIN_EXAMPLE
44
+ resme init
45
+ #+END_EXAMPLE
46
+
47
+ which generates a YML template for your resume in the current directory.
48
+ Comments in the YML file should help you fill the various entries.
49
+ Notice that most entries are optional and you can remove sections which
50
+ are not relevant for your resume.
51
+
52
+ You can now generate a resume using one of the existing formats:
53
+
54
+ #+begin_example
55
+ resme --to org resume.yml
56
+ #+end_example
57
+
58
+ Supported formats include:
59
+
60
+ - =org=: Org Mode format
61
+ - =md=: Markdown format
62
+ - =europass=: Europass format
63
+ (http://interop.europass.cedefop.europa.eu/web-services/remote-upload/)
64
+ - =json=: JSON format (https://jsonresume.org/)
65
+
66
+ If you are not satisfied with the provided templates, you can write
67
+ your own (see below).
68
+
69
+ Notice that you can specify more than one YML file in input. This allows you to
70
+ store data about your resume in different files, if you like to do so
71
+ (e.g., work experiences could be in one file and talks in another).
72
+ The YML files are merged before processing them.
73
+
74
+ Full syntax:
75
+
76
+ #+begin_src shell :results raw output :wrap example
77
+ resme help
78
+ #+end_src
79
+
80
+ #+RESULTS:
81
+ #+begin_example
82
+ resme command [options] [args]
83
+ Available commands:
84
+ view --template FORMAT # Print template used for format
85
+ console # enter the console
86
+ man # print resme manual page
87
+ help [command] # print command usage
88
+ init [options] # generate an empty resume.yml file
89
+ check resume.yml # Check syntax of resume.yml
90
+ list resume.yml # List main sections in resume.yml
91
+ version # print version information
92
+ generate [options] resume.yml ... # output resume
93
+ #+end_example
94
+
95
+ ** Checking validity
96
+ :PROPERTIES:
97
+ :CUSTOM_ID: checking-validity
98
+ :END:
99
+
100
+ Use the =check= command to verify whether your YAML file conforms with
101
+ the intended syntax.
102
+
103
+ #+BEGIN_SRC ruby
104
+ resme check resume.yaml
105
+ #+END_SRC
106
+
107
+ ** Dates in the resume
108
+ :PROPERTIES:
109
+ :CUSTOM_ID: dates-in-the-resume
110
+ :END:
111
+
112
+ Enter dates in the resume in one of the following formats:
113
+
114
+ - Any format accepted by Ruby for a full date (year, month, day)
115
+ - YYYY-MM-DD
116
+ - YYYY-MM
117
+ - YYYY
118
+
119
+ The third and the forth format allows you to enter "partial" dates
120
+ (e.g., when the month or the day have been forgotten or are irrelevant).
121
+
122
+ ** Creating your own templates
123
+ :PROPERTIES:
124
+ :CUSTOM_ID: creating-your-own-templates
125
+ :END:
126
+
127
+ The resumes are generated from the YML matter using ERB templates. The
128
+ provided output formats should support different back ends. Org Mode
129
+ and Markdown, for instance, easily allow for generation of PDFs, HTML,
130
+ and ODT, to mention a few.
131
+
132
+ However, if you want, you can define your own templates.
133
+
134
+ Use the command =view= to print one of the templates used by =resme=
135
+ and build from that.
136
+
137
+ #+begin_example
138
+ resme view --template md
139
+ #+end_example
140
+
141
+ Notice that all the data in the resume is made available in the =data=
142
+ variable. Thus, for instance, the following code snippets generates a
143
+ list of all the work experiences:
144
+
145
+ #+BEGIN_EXAMPLE
146
+ <% data["work"] each do |exp| %>
147
+ - <%= exp["who"] %>
148
+ From: <%= exp["from"] %> till: <%= exp["till"] %>
149
+ <% end %>
150
+ #+END_EXAMPLE
151
+
152
+ To specify your own ERB template to build your resume use the option
153
+ =-e= (=--erb=). Thus, for instance:
154
+
155
+ #+BEGIN_EXAMPLE
156
+ $ resme render -e template.md.erb [-o output_filename] file.yaml ...
157
+ #+END_EXAMPLE
158
+
159
+ uses =template.md.erb= to generate the resume.
160
+
161
+ Some functions can be used in the templates to better control the
162
+ output.
163
+
164
+ String manipulation functions:
165
+
166
+ - =clean string= removes any space at the beginning of =string=
167
+ - =reflow string, n= makes =string= into an array of strings of length
168
+ lower or equal to =n= (useful if you are outputting a textual format,
169
+ for instance.
170
+
171
+ Dates manipulation functions:
172
+
173
+ - =period= generates a string recapping a period. The function abstracts
174
+ different syntax you can use for entries (i.e., =date= or =from= and
175
+ =till=) and different values for the entries (e.g., a missing value
176
+ for =till=)
177
+ - =year string=, =month string=, =day string= return, respectively the
178
+ year, month and day from strings in the format =YYYY-MM-DD=s
179
+ - =has_month input= returns true if =input= has a month, that is, it is
180
+ a date or it is in the form =YYYY-MM=
181
+ - =has_day input= returns true if =input= has a day, that is, it is a
182
+ date or it is in the form =YYYY-MM-DD=
183
+
184
+ You can find the templates in =lib/resme/templates=. These might be good
185
+ starting points if you want to develop your own.
186
+
187
+ ** Contributing your templates
188
+ :PROPERTIES:
189
+ :CUSTOM_ID: contributing-your-templates
190
+ :END:
191
+
192
+ If you develop an output template and want to make it available, please
193
+ let me know, so that I can include it in future releases of this gem.
194
+
195
+ ** Development
196
+ :PROPERTIES:
197
+ :CUSTOM_ID: development
198
+ :END:
199
+
200
+ After checking out the repo, run =bin/setup= to install dependencies.
201
+ You can also run =bin/console= for an interactive prompt that will allow
202
+ you to experiment.
203
+
204
+ To install this gem onto your local machine, run
205
+ =bundle exec rake install=. To release a new version, update the version
206
+ number in =version.rb=, and then run =bundle exec rake release=, which
207
+ will create a git tag for the version, push git commits and tags, and
208
+ push the =.gem= file to [[https://rubygems.org][rubygems.org]].
209
+
210
+ ** Contributing
211
+ :PROPERTIES:
212
+ :CUSTOM_ID: contributing
213
+ :END:
214
+
215
+ Bug reports and pull requests are welcome on GitHub at
216
+ https://github.com/avillafiorita/resme.
217
+
218
+ ** License
219
+ :PROPERTIES:
220
+ :CUSTOM_ID: license
221
+ :END:
222
+
223
+ The gem is available as open source under the terms of the
224
+ [[http://opensource.org/licenses/MIT][MIT License]].
225
+
226
+ ** Change Log
227
+
228
+ In =doc/changelog.org=
229
+
230
+ ** Roadmap
231
+ :PROPERTIES:
232
+ :CUSTOM_ID: roadmap
233
+ :END:
234
+
235
+ In =doc/todo.org= ... guess what is my preferred editor!
236
+
237
+ ** Bugs
238
+ :PROPERTIES:
239
+ :CUSTOM_ID: bugs
240
+ :END:
241
+
242
+ There are still slight differences in the syntax of entries and in the
243
+ way in which the information is formatted in various output formats. For
244
+ instance, gender and birthdate are used in the Europass format, but not
245
+ in the Markdown format. This is in part due to the different standards
246
+ and in part due to personal preferences.
247
+
248
+ *Entries are not sorted by date before outputting them. Make sure you
249
+ put them in the order you want them to appear in your resume.*
250
+
251
+ Unknown number of unknown bugs.
252
+
253
+ ** Release History
254
+ :PROPERTIES:
255
+ :CUSTOM_ID: release-history
256
+ :END:
257
+
258
+ - *0.4.0* refactors all generation commands under =generate=, provides
259
+ new filtering options, adds =-e= option (for custom templates), and
260
+ refactors various portions of code. It also revises this document
261
+ and fixes some minor bugs.
262
+ - *0.3.2* and *0.3.1* fix errors with the Europass format: lists of
263
+ projects, interests, ... are now properly formatted.
264
+ - *0.3* introduces output to org-mode, introduces references for the CV,
265
+ improves output to JSON, adds a =check= command, removes useless blank
266
+ lines in the output, supports =-%>= in the ERB templates, fixes
267
+ various typos in the documentation, introduces various new formatting
268
+ functions, to simplify template generation
269
+ - *0.2* improves output of volunteering activities and other information
270
+ in the Europass and *significantly improves error and warning
271
+ reporting*
272
+ - *0.1* is the first release
data/exe/resme CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'resme'
3
+ require "resme"
4
4
 
5
5
  # read-eval-print-step passing all commands and the argument
6
- Resme::CommandSemantics.reps Resme::CommandSyntax.commands, ARGV
6
+ Resme::CommandSemantics.execute Resme::CommandSyntax.commands, ARGV
@@ -1,90 +1,89 @@
1
- require 'resme/version'
2
- require 'readline'
3
- require 'fileutils'
4
- require 'date'
5
- require 'yaml'
6
- require 'erb'
7
- require 'json'
8
- require 'kwalify'
1
+ require "resme/version"
2
+ require "readline"
3
+ require "fileutils"
4
+ require "date"
5
+ require "yaml"
6
+ require "erb"
7
+ require "json"
9
8
 
10
9
  module Resme
11
10
  module CommandSemantics
12
- APPNAME = 'resme'
11
+ APPNAME = "resme"
13
12
  VERSION = Resme::VERSION
14
13
 
15
14
  #
16
15
  # Main App Starts Here!
17
16
  #
18
- def self.version opts = nil, argv = []
17
+ def self.version(opts = nil, argv = [])
19
18
  puts "#{APPNAME} version #{VERSION}"
20
19
  end
21
20
 
22
- def self.man opts = nil, argv = []
23
- path = File.join(File.dirname(__FILE__), "/../../../README.md")
21
+ def self.man(opts = nil, argv = [])
22
+ path = File.join(File.dirname(__FILE__), "/../../../README.org")
24
23
  file = File.open(path, "r")
25
- contents = file.read
26
- puts contents
24
+ puts file.read
27
25
  end
28
26
 
29
- def self.help opts = nil, argv = []
27
+ def self.help(opts = nil, argv = [])
30
28
  all_commands = CommandSyntax.commands
31
29
 
32
30
  if argv != []
33
- argv.map { |x| puts all_commands[x.to_sym][2] }
31
+ argv.map do |x|
32
+ puts all_commands[x.to_sym][:help]
33
+ puts "\n\n"
34
+ end
34
35
  else
35
- puts "#{APPNAME} command [options] [args]"
36
- puts ""
37
- puts "Available commands:"
38
- puts ""
39
- all_commands.keys.each do |key|
40
- puts " " + all_commands[key][0].banner
36
+ puts "#{APPNAME} command [options] [args]\n"
37
+ puts "Available commands:\n"
38
+ all_commands.each_key do |key|
39
+ puts " #{all_commands[key][:options].banner}"
41
40
  end
42
41
  end
43
42
  end
44
43
 
45
- def self.console opts, argv = []
44
+ def self.console(opts, argv = [])
46
45
  all_commands = CommandSyntax.commands
47
46
  all_commands.delete(:console)
48
47
 
49
48
  i = 0
50
49
  while true
51
50
  string = Readline.readline("#{APPNAME}:%03d> " % i, true)
52
- string.gsub!(/^#{APPNAME} /, "") # as a courtesy, remove any leading appname string
53
- if string == "exit" or string == "quit" or string == "." then
54
- exit 0
55
- end
56
- reps all_commands, string.split(' ')
51
+ # as a courtesy, remove any leading appname string
52
+ string.gsub!(/^#{APPNAME} /, "")
53
+ exit 0 if %w[exit quit .].include? string
54
+ execute all_commands, string.split(" ")
57
55
  i = i + 1
58
56
  end
59
57
  end
60
58
 
61
59
  # read-eval-print step
62
- def self.reps all_commands, argv
60
+ # check if argv is in any of all_commands, parse options
61
+ # according to the specification in all_commands and invoke
62
+ # a function in this class to actually do the work
63
+ def self.execute(all_commands, argv)
63
64
  if argv == [] or argv[0] == "--help" or argv[0] == "-h"
64
- CommandSemantics.help
65
+ help
65
66
  exit 0
66
67
  else
67
68
  command = argv[0]
68
- syntax_and_semantics = all_commands[command.to_sym]
69
- if syntax_and_semantics
70
- opts = syntax_and_semantics[0]
71
- function = syntax_and_semantics[1]
72
-
73
- begin
74
- parser = Slop::Parser.new(opts)
69
+ command_spec = all_commands[command.to_sym]
75
70
 
76
- result = parser.parse(argv[1..-1])
77
- options = result.to_hash
78
- arguments = result.arguments
71
+ if command_spec
72
+ command_name = command_spec[:name]
73
+ option_parser = command_spec[:options]
79
74
 
80
- eval "CommandSemantics::#{function}(options, arguments)"
81
- rescue Slop::Error => e
82
- puts "#{APPNAME}: #{e}"
75
+ begin
76
+ argv.shift # remove command name from ARGV
77
+ options = {}
78
+ parser = option_parser.parse!(into: options)
79
+ eval "CommandSemantics::#{command_name}(options, argv)"
83
80
  rescue Exception => e
84
- puts e
81
+ puts "#{APPNAME} error: #{e}"
82
+ puts "Help with \"#{APPNAME} help #{command_name}\""
85
83
  end
86
84
  else
87
- puts "#{APPNAME}: '#{command}' is not a valid command. See '#{APPNAME} help'"
85
+ puts "#{APPNAME} error: "#{command}" is not a valid command."
86
+ puts "List commands with \"#{APPNAME} help\"."
88
87
  end
89
88
  end
90
89
  end
@@ -92,40 +91,37 @@ module Resme
92
91
  #
93
92
  # APP SPECIFIC COMMANDS
94
93
  #
95
- def self.check opts, argv
96
- schema = Kwalify::Yaml.load_file(File.join(File.dirname(__FILE__), "/../templates/schema.yml"))
97
- ## or
98
- # schema = YAML.load_file('schema.yaml')
99
-
100
- ## create validator
101
- validator = Kwalify::Validator.new(schema)
102
-
103
- ## load document
104
- document = Kwalify::Yaml.load_file(argv[0])
105
- ## or
106
- #document = YAML.load_file('document.yaml')
107
-
108
- ## validate
109
- errors = validator.validate(document)
110
-
111
- ## show errors
112
- if errors && !errors.empty?
113
- for e in errors
114
- puts "[#{e.path}] #{e.message}"
94
+ def self.check(opts, argv)
95
+ begin
96
+ document = YAML.load_file(argv[0], permitted_classes: [Date])
97
+ rescue Psych::SyntaxError => ex
98
+ puts "The file #{argv[0]} has an invalid structure."
99
+ puts ex.message
100
+ exit 1
101
+ end
102
+
103
+ begin
104
+ errors = ResumeStructureValidator.validate(document)
105
+ rescue Exception => ex
106
+ puts "The files #{argv[0]} does not validate"
107
+ ex.entries.each do |error|
108
+ puts "#{error[:full_path]}: #{error[:message]}"
115
109
  end
116
- else
117
- puts "The file #{argv[0]} validates."
110
+ exit 1
118
111
  end
112
+ puts "The file #{argv[0]} has a valid structure."
119
113
  end
120
114
 
121
- def self.init opts, argv
115
+ def self.init(opts, argv)
122
116
  output = opts[:output] || "resume.yml"
123
117
  force = opts[:force]
124
- template = File.join(File.dirname(__FILE__), "/../templates/resume.yml")
118
+ path = File.dirname(__FILE__), "/../templates/resume.yml"
119
+ template = File.join(path)
125
120
 
126
- # avoid catastrophy
127
- if File.exist?(output) and not force
128
- puts "Error: file #{output} already exists. Use --force if you want to overwrite it"
121
+ # avoid catastrophe
122
+ if File.exist?(output) && !force
123
+ puts "#{APPNAME} error: file #{output} already exists."
124
+ puts "Use --force if you want to overwrite it."
129
125
  else
130
126
  content = File.read(template)
131
127
  backup_and_write output, content
@@ -133,60 +129,71 @@ module Resme
133
129
  end
134
130
  end
135
131
 
136
- def self.md opts, argv
137
- output = opts[:output] || "resume-#{Date.today}.md"
138
- template = File.join(File.dirname(__FILE__), "/../templates/resume.md.erb")
139
-
140
- render argv, template, output
141
- puts "Resume generated in #{output}"
132
+ def self.list(opts, argv)
133
+ data = {}
134
+ argv.each do |file|
135
+ data = data.merge(YAML.load_file(file, permitted_classes: [Date]))
136
+ end
137
+ puts "Sections included in #{argv.join(", ")}:"
138
+ data.keys.each do |key|
139
+ puts "- #{key}: #{(data[key] || []).size} entries"
140
+ end
142
141
  end
143
142
 
144
- def self.org opts, argv
145
- output = opts[:output] || "resume-#{Date.today}.org"
146
- template = File.join(File.dirname(__FILE__), "/../templates/resume.org.erb")
147
-
148
- render argv, template, output
149
- puts "Resume generated in #{output}"
143
+ def self.view(opts, argv)
144
+ format = opts[:template] == "europass" ? "xml" : opts[:template]
145
+ template = File.join(File.dirname(__FILE__), "/../templates/resume.#{format}.erb")
146
+ puts File.read template
150
147
  end
151
148
 
152
- def self.json opts, argv
153
- output = opts[:output] || "resume-#{Date.today}.json"
154
- template = File.join(File.dirname(__FILE__), "/../templates/resume.json.erb")
149
+ def self.generate(opts, argv)
150
+ format = opts[:to] == "europass" ? "xml" : opts[:to]
151
+ output = opts[:output] || "resume-#{Date.today}.#{format}"
155
152
 
156
- render argv, template, output
157
- puts "Resume generated in #{output}"
158
- end
153
+ if opts[:erb]
154
+ template = opts[:erb]
155
+ else
156
+ template = File.join(File.dirname(__FILE__), "/../templates/resume.#{format}.erb")
157
+ end
159
158
 
160
- def self.europass opts, argv
161
- output = opts[:output] || "resume-#{Date.today}.xml"
162
- template = File.join(File.dirname(__FILE__), "/../templates/europass/eu.xml.erb")
159
+ skipped_sections = opts[:skip] || []
163
160
 
164
- render argv, template, output
161
+ if !File.exists?(template)
162
+ puts "#{APPNAME} error: format #{format} is not understood."
163
+ end
164
+
165
+ render argv, template, output, skipped_sections
165
166
  puts "Resume generated in #{output}"
166
- puts "Render via, e.g., http://interop.europass.cedefop.europa.eu/web-services/remote-upload/"
167
+
168
+ if format == "xml" then
169
+ puts "Europass XML generated. Render via, e.g., http://interop.europass.cedefop.europa.eu/web-services/remote-upload/"
170
+ end
167
171
  end
168
172
 
169
173
  private
170
174
 
171
- def self.render yml_files, template_filename, output_filename
172
- data = Hash.new
175
+ def self.render(yml_files, template_name, output_name, skipped_sections)
176
+ data = {}
173
177
  yml_files.each do |file|
174
- data = data.merge(YAML.load_file(file))
178
+ data = data.merge(YAML.load_file(file, permitted_classes: [Date]))
179
+ end
180
+ skipped_sections.each do |section|
181
+ data.reject! { |k| k == section }
175
182
  end
176
- template = File.read(template_filename)
177
- output = ERB.new(template, nil, '-').result(binding)
183
+ template = File.read(template_name)
184
+ output = ERB.new(template, trim_mode: "-").result(binding)
178
185
  # it is difficult to write readable ERBs with no empty lines...
179
186
  # we use gsub to replace multiple empty lines with \n\n in the final output
180
187
  output.gsub!(/([\t ]*\n){3,}/, "\n\n")
181
- backup_and_write output_filename, output
188
+ backup_and_write output_name, output
182
189
  end
183
190
 
184
- def self.backup filename
191
+ def self.backup(filename)
185
192
  FileUtils::cp filename, filename + "~"
186
193
  puts "Backup copy #{filename} created in #{filename}~."
187
194
  end
188
195
 
189
- def self.backup_and_write filename, content
196
+ def self.backup_and_write(filename, content)
190
197
  backup(filename) if File.exist?(filename)
191
198
  File.open(filename, "w") { |f| f.puts content }
192
199
  end