resme 1.0.0 → 2.0.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 +4 -4
- data/CHANGELOG.org +27 -0
- data/CODE_OF_CONDUCT.org +16 -0
- data/LICENSE.txt +1 -1
- data/README.org +22 -131
- data/Rakefile +11 -1
- data/TAGS +83 -0
- data/exe/resme +2 -2
- data/lib/resme/cli.rb +80 -0
- data/lib/resme/document.rb +37 -0
- data/lib/resme/document_validator.rb +308 -0
- data/lib/resme/executor.rb +90 -0
- data/lib/resme/helper.rb +211 -0
- data/lib/resme/json_resume.rb +156 -0
- data/lib/resme/version.rb +1 -2
- data/lib/resme.rb +20 -5
- data/sig/resme.rbs +4 -0
- data/templates/json.erb +1 -0
- data/templates/org.erb +217 -0
- data/{lib/resme/templates → templates}/resume.yml +38 -33
- metadata +22 -53
- data/.gitignore +0 -8
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -20
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/lib/resme/cli/command_semantics.rb +0 -198
- data/lib/resme/cli/command_syntax.rb +0 -282
- data/lib/resme/cli/resume_structure_validator.rb +0 -293
- data/lib/resme/renderer/renderer.rb +0 -134
- data/lib/resme/templates/resume.json.erb +0 -155
- data/lib/resme/templates/resume.md.erb +0 -160
- data/lib/resme/templates/resume.org.erb +0 -205
- data/resme.gemspec +0 -34
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Resme
|
|
4
|
+
#
|
|
5
|
+
# Validate a document
|
|
6
|
+
#
|
|
7
|
+
class DocumentValidator
|
|
8
|
+
def initialize(document)
|
|
9
|
+
@document = document
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def validate
|
|
13
|
+
begin
|
|
14
|
+
errors = []
|
|
15
|
+
ClassyHash.validate(
|
|
16
|
+
@document.data,
|
|
17
|
+
SCHEMA,
|
|
18
|
+
errors: errors,
|
|
19
|
+
strict: true,
|
|
20
|
+
raise_errors: true,
|
|
21
|
+
full: true
|
|
22
|
+
)
|
|
23
|
+
errors
|
|
24
|
+
rescue Exception => ex
|
|
25
|
+
puts "#{@document.filename} does not validate:"
|
|
26
|
+
ex.entries.each do |error|
|
|
27
|
+
puts "- #{error[:full_path]}: #{error[:message]}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
OPTIONAL_STRING = [:optional, String, NilClass]
|
|
33
|
+
OPTIONAL_PARTIAL_DATE = [:optional, Date, String, Integer, NilClass]
|
|
34
|
+
PARTIAL_DATE = [Date, String, Integer]
|
|
35
|
+
|
|
36
|
+
#
|
|
37
|
+
# This defines the structure of resume.yml
|
|
38
|
+
# We validate it with ClassyHash
|
|
39
|
+
#
|
|
40
|
+
SCHEMA = {
|
|
41
|
+
basics: {
|
|
42
|
+
first_name: String,
|
|
43
|
+
middle_name: OPTIONAL_STRING,
|
|
44
|
+
last_name: String,
|
|
45
|
+
title: OPTIONAL_STRING,
|
|
46
|
+
picture: OPTIONAL_STRING,
|
|
47
|
+
birthdate: [:optional, Date, NilClass],
|
|
48
|
+
nationality: OPTIONAL_STRING,
|
|
49
|
+
marital_status: OPTIONAL_STRING,
|
|
50
|
+
gender: OPTIONAL_STRING
|
|
51
|
+
},
|
|
52
|
+
contacts: [[ { label: String, value: String } ]],
|
|
53
|
+
addresses: [[
|
|
54
|
+
{
|
|
55
|
+
label: String,
|
|
56
|
+
street: OPTIONAL_STRING,
|
|
57
|
+
postal_code: [:optional, String, Integer, NilClass],
|
|
58
|
+
city: OPTIONAL_STRING,
|
|
59
|
+
region: OPTIONAL_STRING,
|
|
60
|
+
country_code: OPTIONAL_STRING,
|
|
61
|
+
country: OPTIONAL_STRING
|
|
62
|
+
}
|
|
63
|
+
]],
|
|
64
|
+
web_presence: [:optional,
|
|
65
|
+
[[
|
|
66
|
+
{
|
|
67
|
+
label: String,
|
|
68
|
+
value: String
|
|
69
|
+
},
|
|
70
|
+
]],
|
|
71
|
+
NilClass
|
|
72
|
+
],
|
|
73
|
+
summary: OPTIONAL_STRING,
|
|
74
|
+
work: [:optional,
|
|
75
|
+
[[
|
|
76
|
+
{
|
|
77
|
+
name: OPTIONAL_STRING,
|
|
78
|
+
url: OPTIONAL_STRING,
|
|
79
|
+
address: OPTIONAL_STRING,
|
|
80
|
+
end_date: OPTIONAL_PARTIAL_DATE,
|
|
81
|
+
start_date: OPTIONAL_PARTIAL_DATE,
|
|
82
|
+
position: String,
|
|
83
|
+
summary: String,
|
|
84
|
+
details: OPTIONAL_STRING
|
|
85
|
+
},
|
|
86
|
+
]],
|
|
87
|
+
NilClass
|
|
88
|
+
],
|
|
89
|
+
teaching: [:optional,
|
|
90
|
+
[[
|
|
91
|
+
{
|
|
92
|
+
name: String,
|
|
93
|
+
school: OPTIONAL_STRING,
|
|
94
|
+
address: OPTIONAL_STRING,
|
|
95
|
+
end_date: OPTIONAL_PARTIAL_DATE,
|
|
96
|
+
start_date: OPTIONAL_PARTIAL_DATE,
|
|
97
|
+
position: String,
|
|
98
|
+
subject: String,
|
|
99
|
+
summary: OPTIONAL_STRING,
|
|
100
|
+
details: OPTIONAL_STRING
|
|
101
|
+
}
|
|
102
|
+
]],
|
|
103
|
+
NilClass
|
|
104
|
+
],
|
|
105
|
+
projects: [:optional,
|
|
106
|
+
[[
|
|
107
|
+
{
|
|
108
|
+
name: String,
|
|
109
|
+
size: OPTIONAL_STRING,
|
|
110
|
+
stakeholder: OPTIONAL_STRING,
|
|
111
|
+
end_date: OPTIONAL_PARTIAL_DATE,
|
|
112
|
+
start_date: OPTIONAL_PARTIAL_DATE,
|
|
113
|
+
position: String,
|
|
114
|
+
summary: OPTIONAL_STRING,
|
|
115
|
+
}
|
|
116
|
+
]],
|
|
117
|
+
NilClass
|
|
118
|
+
],
|
|
119
|
+
other: [:optional,
|
|
120
|
+
[[
|
|
121
|
+
{
|
|
122
|
+
name: OPTIONAL_STRING,
|
|
123
|
+
end_date: OPTIONAL_PARTIAL_DATE,
|
|
124
|
+
start_date: OPTIONAL_PARTIAL_DATE,
|
|
125
|
+
position: String,
|
|
126
|
+
summary: OPTIONAL_STRING,
|
|
127
|
+
}
|
|
128
|
+
]],
|
|
129
|
+
NilClass
|
|
130
|
+
],
|
|
131
|
+
committees: [:optional,
|
|
132
|
+
[[
|
|
133
|
+
{
|
|
134
|
+
name: String,
|
|
135
|
+
position: String,
|
|
136
|
+
editions: [String, Integer],
|
|
137
|
+
url: OPTIONAL_STRING,
|
|
138
|
+
}
|
|
139
|
+
]],
|
|
140
|
+
NilClass
|
|
141
|
+
],
|
|
142
|
+
volunteer: [:optional,
|
|
143
|
+
[[
|
|
144
|
+
{
|
|
145
|
+
name: String,
|
|
146
|
+
where: OPTIONAL_STRING,
|
|
147
|
+
date: OPTIONAL_PARTIAL_DATE,
|
|
148
|
+
end_date: OPTIONAL_PARTIAL_DATE,
|
|
149
|
+
start_date: OPTIONAL_PARTIAL_DATE,
|
|
150
|
+
position: String,
|
|
151
|
+
summary: OPTIONAL_STRING,
|
|
152
|
+
}
|
|
153
|
+
]],
|
|
154
|
+
NilClass
|
|
155
|
+
],
|
|
156
|
+
visits: [:optional,
|
|
157
|
+
[[
|
|
158
|
+
{
|
|
159
|
+
name: String,
|
|
160
|
+
address: OPTIONAL_STRING,
|
|
161
|
+
end_date: OPTIONAL_PARTIAL_DATE,
|
|
162
|
+
start_date: OPTIONAL_PARTIAL_DATE,
|
|
163
|
+
position: String,
|
|
164
|
+
summary: OPTIONAL_STRING,
|
|
165
|
+
}
|
|
166
|
+
]],
|
|
167
|
+
NilClass
|
|
168
|
+
],
|
|
169
|
+
education: [:optional,
|
|
170
|
+
[[
|
|
171
|
+
{
|
|
172
|
+
degree: OPTIONAL_STRING,
|
|
173
|
+
topic: OPTIONAL_STRING,
|
|
174
|
+
school: String,
|
|
175
|
+
address: OPTIONAL_STRING,
|
|
176
|
+
date: OPTIONAL_PARTIAL_DATE,
|
|
177
|
+
end_date: OPTIONAL_PARTIAL_DATE,
|
|
178
|
+
start_date: OPTIONAL_PARTIAL_DATE,
|
|
179
|
+
url: OPTIONAL_STRING,
|
|
180
|
+
publish: TrueClass,
|
|
181
|
+
score: [:optional, String, Integer, NilClass],
|
|
182
|
+
}
|
|
183
|
+
]],
|
|
184
|
+
NilClass
|
|
185
|
+
],
|
|
186
|
+
publications: [:optional,
|
|
187
|
+
[[
|
|
188
|
+
{
|
|
189
|
+
title: String,
|
|
190
|
+
authors: String,
|
|
191
|
+
publisher: String,
|
|
192
|
+
date: PARTIAL_DATE,
|
|
193
|
+
url: OPTIONAL_STRING,
|
|
194
|
+
}
|
|
195
|
+
]],
|
|
196
|
+
NilClass
|
|
197
|
+
],
|
|
198
|
+
talks: [:optional,
|
|
199
|
+
[[
|
|
200
|
+
{
|
|
201
|
+
title: String,
|
|
202
|
+
venue: String,
|
|
203
|
+
date: PARTIAL_DATE,
|
|
204
|
+
url: OPTIONAL_STRING,
|
|
205
|
+
}
|
|
206
|
+
]],
|
|
207
|
+
NilClass
|
|
208
|
+
],
|
|
209
|
+
awards: [:optional,
|
|
210
|
+
[[
|
|
211
|
+
{
|
|
212
|
+
name: String,
|
|
213
|
+
address: OPTIONAL_STRING,
|
|
214
|
+
date: PARTIAL_DATE,
|
|
215
|
+
title: String,
|
|
216
|
+
summary: OPTIONAL_STRING
|
|
217
|
+
}
|
|
218
|
+
]],
|
|
219
|
+
NilClass
|
|
220
|
+
],
|
|
221
|
+
achievements: [:optional,
|
|
222
|
+
[[
|
|
223
|
+
{
|
|
224
|
+
name: String,
|
|
225
|
+
address: OPTIONAL_STRING,
|
|
226
|
+
date: OPTIONAL_PARTIAL_DATE,
|
|
227
|
+
title: String,
|
|
228
|
+
summary: OPTIONAL_STRING
|
|
229
|
+
}
|
|
230
|
+
]],
|
|
231
|
+
NilClass
|
|
232
|
+
],
|
|
233
|
+
software: [:optional,
|
|
234
|
+
[[
|
|
235
|
+
{
|
|
236
|
+
title: String,
|
|
237
|
+
url: OPTIONAL_STRING,
|
|
238
|
+
programming_language: OPTIONAL_STRING,
|
|
239
|
+
license: OPTIONAL_STRING,
|
|
240
|
+
position: OPTIONAL_STRING,
|
|
241
|
+
summary: OPTIONAL_STRING,
|
|
242
|
+
}
|
|
243
|
+
]],
|
|
244
|
+
NilClass
|
|
245
|
+
],
|
|
246
|
+
skills: [:optional,
|
|
247
|
+
[[
|
|
248
|
+
{
|
|
249
|
+
name: String,
|
|
250
|
+
level: OPTIONAL_STRING,
|
|
251
|
+
summary: OPTIONAL_STRING,
|
|
252
|
+
}
|
|
253
|
+
]],
|
|
254
|
+
NilClass
|
|
255
|
+
],
|
|
256
|
+
driving: [:optional,
|
|
257
|
+
[[ { license: String, } ]],
|
|
258
|
+
NilClass
|
|
259
|
+
],
|
|
260
|
+
languages: [:optional,
|
|
261
|
+
{
|
|
262
|
+
mother_tongues: [[
|
|
263
|
+
{
|
|
264
|
+
code: OPTIONAL_STRING,
|
|
265
|
+
language: String,
|
|
266
|
+
}
|
|
267
|
+
]],
|
|
268
|
+
foreign: [:optional,
|
|
269
|
+
[[
|
|
270
|
+
{
|
|
271
|
+
code: OPTIONAL_STRING,
|
|
272
|
+
language: String,
|
|
273
|
+
level: OPTIONAL_STRING,
|
|
274
|
+
listening: OPTIONAL_STRING,
|
|
275
|
+
reading: OPTIONAL_STRING,
|
|
276
|
+
spoken_interaction: OPTIONAL_STRING,
|
|
277
|
+
spoken_production: OPTIONAL_STRING,
|
|
278
|
+
writing: OPTIONAL_STRING
|
|
279
|
+
}
|
|
280
|
+
]],
|
|
281
|
+
NilClass
|
|
282
|
+
]
|
|
283
|
+
},
|
|
284
|
+
NilClass
|
|
285
|
+
],
|
|
286
|
+
interests: [:optional,
|
|
287
|
+
[[
|
|
288
|
+
{
|
|
289
|
+
name: String,
|
|
290
|
+
level: OPTIONAL_STRING,
|
|
291
|
+
summary: OPTIONAL_STRING,
|
|
292
|
+
}
|
|
293
|
+
]],
|
|
294
|
+
NilClass
|
|
295
|
+
],
|
|
296
|
+
references: [:optional,
|
|
297
|
+
[[
|
|
298
|
+
{
|
|
299
|
+
name: String,
|
|
300
|
+
reference: String,
|
|
301
|
+
contacts: [[ {label: String, value: String} ]]
|
|
302
|
+
}
|
|
303
|
+
]],
|
|
304
|
+
NilClass
|
|
305
|
+
]
|
|
306
|
+
}
|
|
307
|
+
end
|
|
308
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Resme
|
|
4
|
+
class Executor
|
|
5
|
+
# include all formatter helpers
|
|
6
|
+
include Resme::Helper
|
|
7
|
+
|
|
8
|
+
def initialize(input)
|
|
9
|
+
@input = input
|
|
10
|
+
@options = Cli.new(@input)
|
|
11
|
+
@options.parse
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute
|
|
15
|
+
case @options.command
|
|
16
|
+
when :init
|
|
17
|
+
resume_template = File.join __dir__, "../../templates/resume.yml"
|
|
18
|
+
puts File.read resume_template
|
|
19
|
+
|
|
20
|
+
when :validate
|
|
21
|
+
filename = @options.options[:filename]
|
|
22
|
+
document = Document.new(filename)
|
|
23
|
+
validator = DocumentValidator.new(document)
|
|
24
|
+
validator.validate
|
|
25
|
+
|
|
26
|
+
when :sections
|
|
27
|
+
filename = @options.options[:filename]
|
|
28
|
+
keys = Document.new(filename).data.keys
|
|
29
|
+
puts keys
|
|
30
|
+
|
|
31
|
+
when :build
|
|
32
|
+
filename = @options.options[:filename]
|
|
33
|
+
data = Document.new(filename).data
|
|
34
|
+
|
|
35
|
+
except = @options.options[:except] || []
|
|
36
|
+
@data = Document.new(filename).data.except(*except)
|
|
37
|
+
|
|
38
|
+
format = @options.options[:format]
|
|
39
|
+
template_name = File.join __dir__, "../../templates/#{format}.erb"
|
|
40
|
+
template = File.read(template_name)
|
|
41
|
+
|
|
42
|
+
# add the json format, if specified
|
|
43
|
+
if format.to_s == "json"
|
|
44
|
+
js = JsonResume.new Document.new(filename)
|
|
45
|
+
@json_resume = js.build
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
output = ERB.new(template, trim_mode: "-").result(binding)
|
|
49
|
+
|
|
50
|
+
# it is difficult to write readable ERBs with no empty lines... we use
|
|
51
|
+
# gsub to replace multiple empty lines with \n\n in the final output
|
|
52
|
+
output.gsub!(/([\t ]*\n){3,}/, "\n\n")
|
|
53
|
+
backup_and_write(output_filename(format), output)
|
|
54
|
+
|
|
55
|
+
when :list
|
|
56
|
+
templates = File.join __dir__, "../../templates/*.erb"
|
|
57
|
+
files = Dir.glob(templates).map { |x| "#{File.basename(x)} => #{x}" }
|
|
58
|
+
puts files.join("\n")
|
|
59
|
+
|
|
60
|
+
when :cat
|
|
61
|
+
template = @options.options[:template]
|
|
62
|
+
template_path = File.join __dir__, "../../templates/#{template}"
|
|
63
|
+
puts File.read template_path
|
|
64
|
+
|
|
65
|
+
when :help
|
|
66
|
+
# nothing to do, OptParser has the context
|
|
67
|
+
|
|
68
|
+
when :version
|
|
69
|
+
puts "This is version: #{Resme::VERSION}"
|
|
70
|
+
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def output_filename(format)
|
|
77
|
+
"resume-#{Date.today.iso8601}.#{format}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def backup(filename)
|
|
81
|
+
FileUtils::cp filename, filename + "~"
|
|
82
|
+
puts "Backup copy #{filename} created in #{filename}~."
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def backup_and_write(filename, content)
|
|
86
|
+
backup(filename) if File.exist?(filename)
|
|
87
|
+
File.open(filename, "w") { |f| f.puts content }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/resme/helper.rb
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Resme
|
|
4
|
+
#
|
|
5
|
+
# Helper functions for renderers
|
|
6
|
+
#
|
|
7
|
+
module Helper
|
|
8
|
+
# Strip and replace extra spaces and special chars with a single space
|
|
9
|
+
def clean(string)
|
|
10
|
+
string.strip.gsub(/[\t\n ]+/, " ")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def full_name(data)
|
|
14
|
+
[
|
|
15
|
+
data.dig(:basics, :first_name),
|
|
16
|
+
data.dig(:basics, :middle_name),
|
|
17
|
+
data.dig(:basics, :last_name),
|
|
18
|
+
].compact.join(" ")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Break a string into substrings of length chars breaking at spaces
|
|
22
|
+
# and newlines
|
|
23
|
+
#
|
|
24
|
+
# - `string` string to reflow
|
|
25
|
+
# - `chars` number of characters to break the string at
|
|
26
|
+
#
|
|
27
|
+
# Returns an array of strings of length < chars (with exceptions for special
|
|
28
|
+
# cases).
|
|
29
|
+
#
|
|
30
|
+
# Special cases: if one line is longer than chars characters, then break at
|
|
31
|
+
# the first space after chars
|
|
32
|
+
#
|
|
33
|
+
def reflow_a(string, line_break: 78)
|
|
34
|
+
# if the string is less than line_break chars, we are done
|
|
35
|
+
return [clean(string)] if string.length < line_break
|
|
36
|
+
|
|
37
|
+
# here the string is longer than line_break chars
|
|
38
|
+
#
|
|
39
|
+
# if the string has a space between 0 and line_break, we break the
|
|
40
|
+
# string and apply recursion
|
|
41
|
+
#
|
|
42
|
+
line_break.downto(0).each do |index|
|
|
43
|
+
if blank?(string[index])
|
|
44
|
+
return [clean(string[0..(index - 1)])] +
|
|
45
|
+
reflow_a(string[(index + 1)..], line_break:)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# here the string does not have a space between 0 and line_break
|
|
50
|
+
#
|
|
51
|
+
# In this case we try to break at the first blank after the string and
|
|
52
|
+
# apply recursion
|
|
53
|
+
#
|
|
54
|
+
line_break.upto(string.length).each do |index|
|
|
55
|
+
if blank?(string[index])
|
|
56
|
+
return [clean(string[0..(index - 1)])] +
|
|
57
|
+
reflow_a(string[(index + 1)..], line_break:)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# here string is longer than line_break and it does not have spaces
|
|
62
|
+
#
|
|
63
|
+
# The only thing left to do is returning the string as it is
|
|
64
|
+
#
|
|
65
|
+
[clean(string)]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def blank?(char)
|
|
69
|
+
[" ", "\t", "\n"].include?(char)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Reflow at +line_break+, returning a string with each line delimited by
|
|
73
|
+
# +delimiter+ (by default newline) and indented with +indent+ spaces.
|
|
74
|
+
#
|
|
75
|
+
def reflow_s(string, line_break: 75, delimiter: "\n", indent: 3)
|
|
76
|
+
indentation = " " * indent
|
|
77
|
+
|
|
78
|
+
# remember PARAGRAPH BREAKS
|
|
79
|
+
paragraphs = string.gsub("\n\n", "{{PBREAK}}").split("{{PBREAK}}")
|
|
80
|
+
|
|
81
|
+
formatted_pars = paragraphs.map do |paragraph|
|
|
82
|
+
reflow_a(paragraph, line_break:).join(delimiter).gsub(/^/, indentation)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
formatted_pars.join("\n\n") + "\n"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Return a string encoding a period (start - end) from a Hash, containing
|
|
89
|
+
# the specificatio of the period. Manage special cases (partial dates,
|
|
90
|
+
# optional dates).
|
|
91
|
+
#
|
|
92
|
+
# Entry is the input Hash, +from+ is the digging specification to get the
|
|
93
|
+
# "start" date, +to+ is the digging specification to get the "end" date.
|
|
94
|
+
# Fallback is a key we use if +from+ or +to+ are not present.
|
|
95
|
+
# All accept a Symbol or an Array.
|
|
96
|
+
#
|
|
97
|
+
# abstract dates at the year level, taking care of periods if from and
|
|
98
|
+
# till are in two different years
|
|
99
|
+
#
|
|
100
|
+
def period(entry, date: :date, from: :start_date, to: :end_date, separator: "--")
|
|
101
|
+
date = entry[date]
|
|
102
|
+
|
|
103
|
+
return format_date(date) if date.is_a?(Date)
|
|
104
|
+
|
|
105
|
+
from = entry.dig(*[from].flatten)
|
|
106
|
+
to = entry.dig(*[to].flatten)
|
|
107
|
+
|
|
108
|
+
[format_date(from), separator, format_date(to)].compact.join(" ")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# make an entry into an itemized list
|
|
112
|
+
#
|
|
113
|
+
# - +entry+ is a hash containing the specification of a position to itemize
|
|
114
|
+
# (e.g., a work period, a degree, ...)
|
|
115
|
+
#
|
|
116
|
+
# - +header+, is an array of symbols, whose values, comma-separated will
|
|
117
|
+
# generate the header line
|
|
118
|
+
#
|
|
119
|
+
# The output is a String along the lines of:
|
|
120
|
+
#
|
|
121
|
+
# - value of key1, value of key2
|
|
122
|
+
# period
|
|
123
|
+
# reflowed summary
|
|
124
|
+
#
|
|
125
|
+
def itemize(entry, header: %i[role who address], summary_key: :summary)
|
|
126
|
+
header = clean header.map { |x| entry[x] }.compact.join(", ")
|
|
127
|
+
|
|
128
|
+
%(- #{header}
|
|
129
|
+
#{period entry}
|
|
130
|
+
#{reflow_to_s entry[summary_key], line_break: 72, indent: 2}
|
|
131
|
+
).gsub(/^ /, "")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Make a Hash into a list of properties, optionally excluding some keys
|
|
135
|
+
# specified in +except:+ (by default +summary+ and +details+)
|
|
136
|
+
def propertify(entry, except: %i[summary details], indent: 3)
|
|
137
|
+
indentation = " " * indent
|
|
138
|
+
|
|
139
|
+
head = ":PROPERTIES:"
|
|
140
|
+
body = entry.except(*except).map { |k, v| ":#{k.upcase}: #{v}" }.join("\n")
|
|
141
|
+
tail = ":END:"
|
|
142
|
+
|
|
143
|
+
"#{head}\n#{body}\n#{tail}\n".gsub!(/^/, indentation)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def select(contacts, label: :email)
|
|
147
|
+
contact = (contacts || []).find { |x| x[:label] == label.to_s }
|
|
148
|
+
contact ? contact[:value] : nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
#
|
|
152
|
+
# Utility functions for managing dates in the form 2015-01-01 and partial
|
|
153
|
+
# dates (e.g., 2015-05, 2015)
|
|
154
|
+
#
|
|
155
|
+
|
|
156
|
+
# Format the input as a date, according to format. If the input is not
|
|
157
|
+
# a date, return it as it (assuming it represents a partial date (e.g.,
|
|
158
|
+
# 2012-01, 2012
|
|
159
|
+
#
|
|
160
|
+
# Special case of an empty date (date == nil) returns ""
|
|
161
|
+
#
|
|
162
|
+
def format_date(date, format: "%Y-%m-%d")
|
|
163
|
+
date.is_a?(Date) ? date.strftime(format) : date.to_s
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def complete_date(incomplete_date, format: "%Y-%m-%d")
|
|
167
|
+
return incomplete_date.strftime(format) if incomplete_date.is_a?(Date)
|
|
168
|
+
|
|
169
|
+
if ymd?(incomplete_date.to_s)
|
|
170
|
+
incomplete_date.to_s
|
|
171
|
+
elsif ym?(incomplete_date.to_s)
|
|
172
|
+
"#{incomplete_date}-01"
|
|
173
|
+
elsif y?(incomplete_date.to_s)
|
|
174
|
+
"#{incomplete_date}-01-01"
|
|
175
|
+
else
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Return the year
|
|
181
|
+
def year(input)
|
|
182
|
+
input.is_a?(Date) ? input.strftime("%Y") : input.to_s[0..3]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Return the month
|
|
186
|
+
def month(input)
|
|
187
|
+
input.is_a?(Date) ? input.strftime("%m") : input.to_s[5..6]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Return the day
|
|
191
|
+
def day(input)
|
|
192
|
+
input.is_a?(Date) ? input.strftime("%d") : input.to_s[8..9]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Is the input in the form YYYY-MM(-..)
|
|
196
|
+
def y?(input)
|
|
197
|
+
input.is_a?(Date) || input.to_s.size == 4
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Is the input in the form YYYY-MM(-..)
|
|
201
|
+
def ym?(input)
|
|
202
|
+
input.is_a?(Date) || input.to_s.size == 7
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Is the input in the form YYYY-MM-DD
|
|
206
|
+
def ymd?(input)
|
|
207
|
+
input.is_a?(Date) || input.to_s.size == 10
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|