resumetools 0.2.7.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.
- data/CHANGES +15 -0
- data/LICENSE +22 -0
- data/README.md +202 -0
- data/Rakefile +39 -0
- data/examples/sample.pdf +1532 -0
- data/examples/sample.resume +137 -0
- data/lib/fonts/Vera.ttf +0 -0
- data/lib/fonts/VeraBI.ttf +0 -0
- data/lib/fonts/VeraBd.ttf +0 -0
- data/lib/fonts/VeraIt.ttf +0 -0
- data/lib/resumetools.rb +37 -0
- data/lib/resumetools/grammars/resume.treetop +389 -0
- data/lib/resumetools/resume/export.rb +125 -0
- data/lib/resumetools/resume/json.rb +79 -0
- data/lib/resumetools/resume/pdf.rb +161 -0
- data/lib/resumetools/resume/plain_text.rb +143 -0
- data/lib/resumetools/resume/resume.rb +307 -0
- data/lib/resumetools/resume/text_reader.rb +107 -0
- data/lib/resumetools/version.rb +36 -0
- data/spec/grammar_spec.rb +93 -0
- data/spec/read_resume_spec.rb +61 -0
- data/spec/rendering_pdf_spec.rb +37 -0
- data/spec/resume_spec.rb +217 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +39 -0
- data/tasks/default.rake +1 -0
- data/tasks/gem.rake +80 -0
- data/tasks/package.rake +9 -0
- data/tasks/rdoc.rake +29 -0
- data/tasks/rspec.rake +16 -0
- metadata +155 -0
@@ -0,0 +1,307 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
#--
|
4
|
+
# Copyright (c) 2009 Virgil Dimaguila
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person
|
7
|
+
# obtaining a copy of this software and associated documentation
|
8
|
+
# files (the "Software"), to deal in the Software without
|
9
|
+
# restriction, including without limitation the rights to use,
|
10
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
# copies of the Software, and to permit persons to whom the
|
12
|
+
# Software is furnished to do so, subject to the following
|
13
|
+
# conditions:
|
14
|
+
#
|
15
|
+
# The above copyright notice and this permission notice shall be
|
16
|
+
# included in all copies or substantial portions of the Software.
|
17
|
+
#
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
20
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
21
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
22
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
23
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
24
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
25
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
26
|
+
#++
|
27
|
+
|
28
|
+
module ResumeTools
|
29
|
+
|
30
|
+
# = Resume
|
31
|
+
# Represents a single resume document
|
32
|
+
class Resume
|
33
|
+
|
34
|
+
# The full name of the subject of this resume
|
35
|
+
attr_accessor :full_name
|
36
|
+
|
37
|
+
# The subject's URL
|
38
|
+
attr_accessor :url
|
39
|
+
|
40
|
+
# The subject's e-mail address
|
41
|
+
attr_accessor :email
|
42
|
+
|
43
|
+
# The subject's telephone number
|
44
|
+
attr_accessor :telephone
|
45
|
+
|
46
|
+
# First line of the address
|
47
|
+
attr_accessor :address1
|
48
|
+
|
49
|
+
# Second line of the address
|
50
|
+
attr_accessor :address2
|
51
|
+
|
52
|
+
# The resume's sections
|
53
|
+
attr_reader :sections
|
54
|
+
|
55
|
+
# Creates a +Resume+ and passes it to the given block. Returns the created +Resume+.
|
56
|
+
def self.build(opts={}, &block)
|
57
|
+
resume = self.new(opts)
|
58
|
+
block.call(resume)
|
59
|
+
resume
|
60
|
+
end
|
61
|
+
|
62
|
+
# Creates a new +Resume+ with the given properties
|
63
|
+
def initialize(props={})
|
64
|
+
@full_name = props[:full_name] || ""
|
65
|
+
@url = props[:url] || ""
|
66
|
+
@email = props[:email] || ""
|
67
|
+
@telephone = props[:telephone] || ""
|
68
|
+
@address1 = props[:address1] || ""
|
69
|
+
@address2 = props[:address2] || ""
|
70
|
+
@sections = Array.new
|
71
|
+
end
|
72
|
+
|
73
|
+
# Add section
|
74
|
+
def add_section(section)
|
75
|
+
self.sections << section
|
76
|
+
self
|
77
|
+
end
|
78
|
+
|
79
|
+
# Create new section and add to sections
|
80
|
+
def create_section(props={}, &block)
|
81
|
+
section = Section.new(props)
|
82
|
+
block.call(section) if block
|
83
|
+
self.add_section(section)
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
def has_url?
|
89
|
+
!self.url.blank?
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
def has_email?
|
94
|
+
!self.email.blank?
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
def has_telephone?
|
99
|
+
!self.telephone.blank?
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
def has_address1?
|
104
|
+
!self.address1.blank?
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
def has_address2?
|
109
|
+
!self.address2.blank?
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
def has_sections?
|
114
|
+
!self.sections.empty?
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns an array of lines that has the contact info
|
118
|
+
def header_lines
|
119
|
+
elements = []
|
120
|
+
[:address1, :address2, :telephone, :email, :url].each do |element|
|
121
|
+
elements << self.send(element) unless self.send(element).blank?
|
122
|
+
end
|
123
|
+
lines = []
|
124
|
+
elements.each_slice(2) { |pair| lines << pair.join(" • ") }
|
125
|
+
lines
|
126
|
+
end
|
127
|
+
|
128
|
+
end #class Resume
|
129
|
+
|
130
|
+
|
131
|
+
# = Section
|
132
|
+
# Represents a section in the resume
|
133
|
+
class Section
|
134
|
+
# Section title
|
135
|
+
attr_accessor :title
|
136
|
+
|
137
|
+
# Section paragraph
|
138
|
+
attr_accessor :para
|
139
|
+
|
140
|
+
# List of periods
|
141
|
+
attr_reader :periods
|
142
|
+
|
143
|
+
# List of items
|
144
|
+
attr_reader :items
|
145
|
+
|
146
|
+
#
|
147
|
+
def initialize(props={})
|
148
|
+
@title = props[:title] || ""
|
149
|
+
@para = props[:para] || ""
|
150
|
+
@items = Array.new
|
151
|
+
@periods = Array.new
|
152
|
+
end
|
153
|
+
|
154
|
+
#
|
155
|
+
def has_paragraph?
|
156
|
+
!self.para.blank?
|
157
|
+
end
|
158
|
+
|
159
|
+
def has_para?
|
160
|
+
self.has_paragraph?
|
161
|
+
end
|
162
|
+
|
163
|
+
#
|
164
|
+
def has_items?
|
165
|
+
!self.items.empty?
|
166
|
+
end
|
167
|
+
|
168
|
+
#
|
169
|
+
def has_periods?
|
170
|
+
!self.periods.empty?
|
171
|
+
end
|
172
|
+
|
173
|
+
#
|
174
|
+
def has_title?
|
175
|
+
!self.title.blank?
|
176
|
+
|
177
|
+
end
|
178
|
+
|
179
|
+
# Creates a period and adds it to the list
|
180
|
+
def create_period(props={}, &block)
|
181
|
+
period = Period.new(props)
|
182
|
+
block.call(period) if block
|
183
|
+
self.add_period(period)
|
184
|
+
self
|
185
|
+
end
|
186
|
+
|
187
|
+
# Adds a period
|
188
|
+
def add_period(period)
|
189
|
+
self.periods << period
|
190
|
+
end
|
191
|
+
|
192
|
+
# Ads an item
|
193
|
+
def add_item(item)
|
194
|
+
self.items << item
|
195
|
+
end
|
196
|
+
|
197
|
+
# Creates an item and adds it to the list
|
198
|
+
def create_item(props={}, &block)
|
199
|
+
item = Item.new(props)
|
200
|
+
block.call(item) if block
|
201
|
+
self.add_item(item)
|
202
|
+
self
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
# = Period
|
208
|
+
# Represents a period in the section
|
209
|
+
class Period
|
210
|
+
# Period title
|
211
|
+
attr_accessor :title
|
212
|
+
|
213
|
+
# Period location
|
214
|
+
attr_accessor :location
|
215
|
+
|
216
|
+
# Period organization
|
217
|
+
attr_accessor :organization
|
218
|
+
|
219
|
+
# Period start date
|
220
|
+
attr_accessor :dtstart
|
221
|
+
|
222
|
+
# Period end date
|
223
|
+
attr_accessor :dtend
|
224
|
+
|
225
|
+
# List of items
|
226
|
+
attr_reader :items
|
227
|
+
|
228
|
+
#
|
229
|
+
def initialize(props={})
|
230
|
+
@title = props[:title] || ""
|
231
|
+
@location = props[:location] || ""
|
232
|
+
@organization = props[:organization] || ""
|
233
|
+
@dtstart = props[:dtstart] || nil
|
234
|
+
@dtend = props[:dtend] || nil
|
235
|
+
@items = Array.new
|
236
|
+
end
|
237
|
+
|
238
|
+
#
|
239
|
+
def has_title?
|
240
|
+
!self.title.blank?
|
241
|
+
end
|
242
|
+
|
243
|
+
def has_location?
|
244
|
+
!self.location.blank?
|
245
|
+
end
|
246
|
+
|
247
|
+
def has_organization?
|
248
|
+
!self.organization.blank?
|
249
|
+
end
|
250
|
+
|
251
|
+
def has_dtstart?
|
252
|
+
!self.dtstart.blank?
|
253
|
+
end
|
254
|
+
|
255
|
+
def has_dtend?
|
256
|
+
!self.dtend.blank?
|
257
|
+
end
|
258
|
+
|
259
|
+
def has_items?
|
260
|
+
!self.items.empty?
|
261
|
+
end
|
262
|
+
|
263
|
+
# Adds an item
|
264
|
+
def add_item(item)
|
265
|
+
self.items << item
|
266
|
+
end
|
267
|
+
|
268
|
+
# Creates an item and adds it to the list
|
269
|
+
def create_item(props={}, &block)
|
270
|
+
item = Item.new(props)
|
271
|
+
block.call(item) if block
|
272
|
+
self.add_item(item)
|
273
|
+
item
|
274
|
+
end
|
275
|
+
|
276
|
+
# The period details in a single line
|
277
|
+
def line
|
278
|
+
elements = []
|
279
|
+
elements << self.organization if self.has_organization?
|
280
|
+
elements << self.location if self.has_location?
|
281
|
+
if self.has_dtstart? && self.has_dtend?
|
282
|
+
date = " (#{self.dtstart} - #{self.dtend})"
|
283
|
+
elsif self.has_dtstart? || self.has_dtend?
|
284
|
+
value = self.has_dtstart? ? self.dtstart : self.dtend
|
285
|
+
date = " (#{value})"
|
286
|
+
else
|
287
|
+
date = ''
|
288
|
+
end
|
289
|
+
elements.join(", ").concat("#{date}")
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
|
294
|
+
# = Item
|
295
|
+
# Represents an item in a period or section. Items are usually
|
296
|
+
# bulleted items that are listed in order
|
297
|
+
class Item
|
298
|
+
# The item text
|
299
|
+
attr_accessor :text
|
300
|
+
|
301
|
+
#
|
302
|
+
def initialize(props={})
|
303
|
+
@text = props[:text] || ""
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2009 Virgil Dimaguila
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person
|
5
|
+
# obtaining a copy of this software and associated documentation
|
6
|
+
# files (the "Software"), to deal in the Software without
|
7
|
+
# restriction, including without limitation the rights to use,
|
8
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the
|
10
|
+
# Software is furnished to do so, subject to the following
|
11
|
+
# conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be
|
14
|
+
# included in all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
18
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
20
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
21
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
22
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
23
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
24
|
+
#++
|
25
|
+
|
26
|
+
module ResumeTools
|
27
|
+
module TextReader
|
28
|
+
class ParseException < Exception
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
require 'treetop'
|
33
|
+
|
34
|
+
Treetop.load File.join(File.dirname(__FILE__), '..', 'grammars', 'resume.treetop')
|
35
|
+
|
36
|
+
# Builds a new Resume instance from text
|
37
|
+
def from_text(text)
|
38
|
+
parser = ::ResumeParser.new
|
39
|
+
result = parser.parse(text)
|
40
|
+
|
41
|
+
if result.nil?
|
42
|
+
raise ParseException.new(parser.failure_reason)
|
43
|
+
end
|
44
|
+
|
45
|
+
resume = ResumeTools::Resume.new
|
46
|
+
current_section = nil
|
47
|
+
current_period = nil
|
48
|
+
|
49
|
+
result.elements.each do |element|
|
50
|
+
case element.data_type
|
51
|
+
when :item
|
52
|
+
item = ::ResumeTools::Item.new(:text => element.value)
|
53
|
+
if current_period
|
54
|
+
current_period.add_item(item)
|
55
|
+
elsif current_section
|
56
|
+
current_section.add_item(item)
|
57
|
+
end
|
58
|
+
when :period
|
59
|
+
current_period = ::ResumeTools::Period.new(:title => element.value)
|
60
|
+
current_section.add_period(current_period) if current_section
|
61
|
+
when :section
|
62
|
+
current_period = nil # Reset period
|
63
|
+
current_section = ::ResumeTools::Section.new(:title => element.value)
|
64
|
+
resume.add_section(current_section)
|
65
|
+
when :period_location
|
66
|
+
current_period.location = element.value if current_period
|
67
|
+
when :period_organization
|
68
|
+
current_period.organization = element.value if current_period
|
69
|
+
when :period_dates
|
70
|
+
if current_period
|
71
|
+
dates = element.value.split("to", 2).map { |d| d.strip }
|
72
|
+
if dates.length == 1
|
73
|
+
current_period.dtend = dates[0]
|
74
|
+
current_period.dtstart = nil
|
75
|
+
elsif dates.length == 2
|
76
|
+
current_period.dtstart = dates[0]
|
77
|
+
current_period.dtend = dates[1]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
when :paragraph
|
81
|
+
current_section.para = element.value if current_section
|
82
|
+
when :contact_name
|
83
|
+
resume.full_name = element.value
|
84
|
+
when :contact_telephone
|
85
|
+
resume.telephone = element.value
|
86
|
+
when :contact_email
|
87
|
+
resume.email = element.value
|
88
|
+
when :contact_address
|
89
|
+
if resume.address1.blank?
|
90
|
+
resume.address1 = element.value
|
91
|
+
elsif resume.address2.blank?
|
92
|
+
resume.address2 = element.value
|
93
|
+
end
|
94
|
+
when :contact_url
|
95
|
+
resume.url = element.value
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
resume
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class << Resume
|
105
|
+
include TextReader::ClassMethods
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#--
|
2
|
+
# Resume Tools, Copyright (c) 2009 Virgil Dimaguila
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person
|
5
|
+
# obtaining a copy of this software and associated documentation
|
6
|
+
# files (the "Software"), to deal in the Software without
|
7
|
+
# restriction, including without limitation the rights to use,
|
8
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the
|
10
|
+
# Software is furnished to do so, subject to the following
|
11
|
+
# conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be
|
14
|
+
# included in all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
18
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
20
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
21
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
22
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
23
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
24
|
+
#++
|
25
|
+
|
26
|
+
unless defined? ResumeTools::VERSION
|
27
|
+
module ResumeTools
|
28
|
+
module VERSION
|
29
|
+
MAJOR = 0
|
30
|
+
MINOR = 2
|
31
|
+
TINY = 7
|
32
|
+
BITSY = 0
|
33
|
+
STRING = [MAJOR, MINOR, TINY, BITSY].join('.')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|