resme 0.3.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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