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