resumetools 0.2.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,125 @@
|
|
1
|
+
module ResumeTools
|
2
|
+
|
3
|
+
class Exporter
|
4
|
+
def initialize(resume)
|
5
|
+
@resume = resume
|
6
|
+
@content = ""
|
7
|
+
end
|
8
|
+
|
9
|
+
def run
|
10
|
+
reset!
|
11
|
+
write_header
|
12
|
+
blank_line
|
13
|
+
|
14
|
+
@resume.sections.each_with_index do |section, n|
|
15
|
+
write_section section
|
16
|
+
blank_line if (n < @resume.sections.length - 1)
|
17
|
+
end
|
18
|
+
|
19
|
+
@content
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def reset!
|
25
|
+
@content = ""
|
26
|
+
end
|
27
|
+
|
28
|
+
def write_header
|
29
|
+
write_contact_info :full_name
|
30
|
+
write_contact_info :address1
|
31
|
+
write_contact_info :address2
|
32
|
+
write_contact_info :telephone
|
33
|
+
write_contact_info :email
|
34
|
+
write_contact_info :url
|
35
|
+
end
|
36
|
+
|
37
|
+
def write_contact_info(info)
|
38
|
+
# TODO: make this smoother
|
39
|
+
case info
|
40
|
+
when :full_name
|
41
|
+
line "#N #{@resume.full_name}" unless @resume.full_name.blank?
|
42
|
+
when :address1
|
43
|
+
line "#A #{@resume.address1}" if @resume.has_address1?
|
44
|
+
when :address2
|
45
|
+
line "#A #{@resume.address2}" if @resume.has_address2?
|
46
|
+
when :telephone
|
47
|
+
line "#T #{@resume.telephone}" if @resume.has_telephone?
|
48
|
+
when :email
|
49
|
+
line "#E #{@resume.email}" if @resume.has_email?
|
50
|
+
when :url
|
51
|
+
line "#U #{@resume.url}" if @resume.has_url?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def write_section(section)
|
56
|
+
line "= #{section.title}"
|
57
|
+
write_para(section.para) if section.has_para?
|
58
|
+
|
59
|
+
section.items.each_with_index do |item, n|
|
60
|
+
write_item item
|
61
|
+
end
|
62
|
+
|
63
|
+
section.periods.each_with_index do |period, n|
|
64
|
+
write_period period
|
65
|
+
blank_line if (n < section.periods.length - 1)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def write_para(para)
|
70
|
+
line "---"
|
71
|
+
line para
|
72
|
+
line "---"
|
73
|
+
end
|
74
|
+
|
75
|
+
def write_item(item)
|
76
|
+
line "- #{item.text}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def write_period(period)
|
80
|
+
line "+ #{period.title}"
|
81
|
+
line ">O #{period.organization}" if period.has_organization?
|
82
|
+
line ">L #{period.location}" if period.has_location?
|
83
|
+
write_dates(period)
|
84
|
+
|
85
|
+
if period.has_items?
|
86
|
+
period.items.each_with_index do |item, n|
|
87
|
+
write_item item
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def write_dates(period)
|
93
|
+
text = ""
|
94
|
+
if period.has_dtstart? && period.has_dtend?
|
95
|
+
text = "#{period.dtstart} to #{period.dtend}"
|
96
|
+
elsif period.has_dtstart? && !period.has_dtend?
|
97
|
+
text = period.dtstart
|
98
|
+
elsif !period.has_dtstart? && period.has_dtend?
|
99
|
+
text = period.dtend
|
100
|
+
else
|
101
|
+
text = ""
|
102
|
+
end
|
103
|
+
line ">D #{text}" unless text.blank?
|
104
|
+
end
|
105
|
+
|
106
|
+
def blank_line
|
107
|
+
@content << "\n"
|
108
|
+
end
|
109
|
+
|
110
|
+
def line(text)
|
111
|
+
@content << text << "\n"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
module Export
|
116
|
+
def export
|
117
|
+
exporter = Exporter.new(self)
|
118
|
+
exporter.run
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
Resume.class_eval do
|
123
|
+
include Export
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'json/pure'
|
2
|
+
|
3
|
+
module ResumeTools
|
4
|
+
module Renderer
|
5
|
+
class JSON
|
6
|
+
def initialize(resume)
|
7
|
+
@resume = resume
|
8
|
+
end
|
9
|
+
|
10
|
+
def render(opts={})
|
11
|
+
frame = {
|
12
|
+
:full_name => @resume.full_name,
|
13
|
+
:url => @resume.url,
|
14
|
+
:email => @resume.email,
|
15
|
+
:telephone => @resume.telephone,
|
16
|
+
:address1 => @resume.address1,
|
17
|
+
:address2 => @resume.address2,
|
18
|
+
:sections => @resume.sections.map { |section| section.json_frame }
|
19
|
+
}
|
20
|
+
::JSON.generate(frame)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module JSONHelpers
|
25
|
+
module SectionMethods
|
26
|
+
def json_frame
|
27
|
+
frame = {
|
28
|
+
:title => self.title,
|
29
|
+
:para => self.para,
|
30
|
+
:items => self.items.map { |item| item.json_frame },
|
31
|
+
:periods => self.periods.map { |period| period.json_frame }
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module ItemMethods
|
37
|
+
def json_frame
|
38
|
+
frame = { :text => self.text }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module PeriodMethods
|
43
|
+
def json_frame
|
44
|
+
frame = {
|
45
|
+
:title => self.title,
|
46
|
+
:location => self.location,
|
47
|
+
:organization => self.organization,
|
48
|
+
:dtstart => self.dtstart,
|
49
|
+
:dtend => self.dtend,
|
50
|
+
:items => self.items.map { |item| item.json_frame }
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
module JSONMethods
|
57
|
+
def render_json(opts={})
|
58
|
+
renderer = JSON.new(self)
|
59
|
+
renderer.render(opts)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Section.class_eval do
|
65
|
+
include Renderer::JSONHelpers::SectionMethods
|
66
|
+
end
|
67
|
+
|
68
|
+
Period.class_eval do
|
69
|
+
include Renderer::JSONHelpers::PeriodMethods
|
70
|
+
end
|
71
|
+
|
72
|
+
Item.class_eval do
|
73
|
+
include Renderer::JSONHelpers::ItemMethods
|
74
|
+
end
|
75
|
+
|
76
|
+
Resume.class_eval do
|
77
|
+
include Renderer::JSONMethods
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,161 @@
|
|
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
|
+
gems = [
|
29
|
+
['prawn', '>= 0.5.1'],
|
30
|
+
['prawn-core', '>= 0.5.1'],
|
31
|
+
['prawn-layout', '>= 0.2.1'],
|
32
|
+
['prawn-format', '>= 0.2.1']
|
33
|
+
]
|
34
|
+
|
35
|
+
gems.each do |name, version|
|
36
|
+
if version
|
37
|
+
gem name, version
|
38
|
+
else
|
39
|
+
gem name
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
require "prawn"
|
44
|
+
require "prawn/format"
|
45
|
+
require "prawn/layout"
|
46
|
+
require "prawn/measurement_extensions"
|
47
|
+
|
48
|
+
module ResumeTools
|
49
|
+
module Renderer
|
50
|
+
module PDF
|
51
|
+
|
52
|
+
FONT_DIR = File.join(File.dirname(__FILE__), '..', '..', 'fonts')
|
53
|
+
MARGINS = [1.0, 1.0, 1.0, 1.0]
|
54
|
+
FONT_SIZES = {
|
55
|
+
:default => 9,
|
56
|
+
:header => 14,
|
57
|
+
:contact => 9,
|
58
|
+
:section => 10,
|
59
|
+
:para => 9,
|
60
|
+
:item => 9,
|
61
|
+
:period => 12
|
62
|
+
}
|
63
|
+
DATE_FORMAT = "%B, %Y"
|
64
|
+
|
65
|
+
# Render to PDF
|
66
|
+
def render_pdf(opts={})
|
67
|
+
pdf = Prawn::Document.new(
|
68
|
+
:info => {},
|
69
|
+
:top_margin => MARGINS[0].in,
|
70
|
+
:left_margin => MARGINS[1].in,
|
71
|
+
:bottom_margin => MARGINS[2].in,
|
72
|
+
:right_margin => MARGINS[3].in
|
73
|
+
)
|
74
|
+
|
75
|
+
pdf.font_families.update(
|
76
|
+
"VeraSans" => {
|
77
|
+
:normal => File.expand_path("Vera.ttf", FONT_DIR),
|
78
|
+
:bold => File.expand_path("VeraBd.ttf", FONT_DIR),
|
79
|
+
:italic => File.expand_path("VeraIt.ttf", FONT_DIR),
|
80
|
+
:bold_italic => File.expand_path("VeraBI.ttf", FONT_DIR)
|
81
|
+
}
|
82
|
+
)
|
83
|
+
|
84
|
+
# Set default font
|
85
|
+
pdf.font("Helvetica", :style => :normal, :size => FONT_SIZES[:default], :kerning => true)
|
86
|
+
|
87
|
+
# Name
|
88
|
+
pdf.text self.full_name, :style => :bold, :size => FONT_SIZES[:header], :align => :center
|
89
|
+
|
90
|
+
# Contact info
|
91
|
+
self.header_lines.each do |line|
|
92
|
+
pdf.text line, :align => :center
|
93
|
+
end
|
94
|
+
|
95
|
+
pdf.pad_bottom 20 do
|
96
|
+
end
|
97
|
+
|
98
|
+
# Sections
|
99
|
+
self.sections.each do |section|
|
100
|
+
pdf.pad_top(20) do
|
101
|
+
# Section title
|
102
|
+
pdf.text section.title, :style => :bold, :size => FONT_SIZES[:section]
|
103
|
+
|
104
|
+
# Section paragraph
|
105
|
+
unless section.para.blank?
|
106
|
+
pdf.span(pdf.bounds.width - 10, :position => 10) do
|
107
|
+
pdf.pad_top(5) { pdf.text section.para, :size => FONT_SIZES[:para] }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Section items
|
112
|
+
unless section.items.empty?
|
113
|
+
pdf.table section.items.map { |item| [" •", item.text] },
|
114
|
+
:font_size => FONT_SIZES[:item],
|
115
|
+
:column_widths => { 0 => 20, 1 => 420 },
|
116
|
+
:border_style => :none,
|
117
|
+
:border_color => "ffffff",
|
118
|
+
:vertical_padding => 4,
|
119
|
+
:horizontal_padding => 0,
|
120
|
+
:align => { 0 => :left, 1 => :left }
|
121
|
+
end
|
122
|
+
|
123
|
+
# Periods
|
124
|
+
section.periods.each do |period|
|
125
|
+
pdf.span(pdf.bounds.width - 10, :position => 10) do
|
126
|
+
pdf.pad_top(5) do
|
127
|
+
# Period title
|
128
|
+
pdf.pad_top(5) { pdf.text period.title, :style => :bold, :size => FONT_SIZES[:period] }
|
129
|
+
|
130
|
+
# Period details
|
131
|
+
pdf.span(pdf.bounds.width - 10, :position => 10) do
|
132
|
+
pdf.text(period.line, :size => FONT_SIZES[:default])
|
133
|
+
end
|
134
|
+
|
135
|
+
# Period items
|
136
|
+
unless period.items.empty?
|
137
|
+
pdf.table period.items.map { |item| [" •", item.text] },
|
138
|
+
:font_size => FONT_SIZES[:item],
|
139
|
+
:column_widths => { 0 => 20, 1 => 420 },
|
140
|
+
:border_style => :none,
|
141
|
+
:border_color => "ffffff",
|
142
|
+
:vertical_padding => 4,
|
143
|
+
:horizontal_padding => 0,
|
144
|
+
:align => { 0 => :center, 1 => :left }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
pdf.render
|
153
|
+
end
|
154
|
+
|
155
|
+
end #module PDF
|
156
|
+
end #module Renderer
|
157
|
+
|
158
|
+
Resume.class_eval do
|
159
|
+
include Renderer::PDF
|
160
|
+
end
|
161
|
+
end #module ResumeTools
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'text/format'
|
2
|
+
|
3
|
+
module ResumeTools
|
4
|
+
module Renderer
|
5
|
+
class PlainText
|
6
|
+
def initialize(resume)
|
7
|
+
@resume = resume
|
8
|
+
end
|
9
|
+
|
10
|
+
def render(opts={})
|
11
|
+
@opts = {
|
12
|
+
:item_bullet => '-',
|
13
|
+
:header_filler => '-',
|
14
|
+
:newline => "\n",
|
15
|
+
:first_indent => 0,
|
16
|
+
:columns => 80,
|
17
|
+
:indent => 2,
|
18
|
+
:centered_top => true,
|
19
|
+
:lined_headers => true
|
20
|
+
}
|
21
|
+
@opts.merge!(opts)
|
22
|
+
@format = Text::Format.new
|
23
|
+
@format.first_indent = @opts[:first_indent]
|
24
|
+
@format.columns = @opts[:columns]
|
25
|
+
|
26
|
+
top_line @resume.full_name unless @resume.full_name.blank?
|
27
|
+
top_line @resume.address1 if @resume.has_address1?
|
28
|
+
top_line @resume.address2 if @resume.has_address2?
|
29
|
+
top_line @resume.telephone if @resume.has_telephone?
|
30
|
+
top_line @resume.email if @resume.has_email?
|
31
|
+
top_line @resume.url if @resume.has_url?
|
32
|
+
blank_line
|
33
|
+
|
34
|
+
@resume.sections.each_with_index do |section, n|
|
35
|
+
render_section(section)
|
36
|
+
blank_line if (n < @resume.sections.length - 1)
|
37
|
+
end
|
38
|
+
content
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def render_section(section)
|
44
|
+
header section.title
|
45
|
+
render_paragraph(section.para) if section.has_paragraph?
|
46
|
+
|
47
|
+
section.items.each_with_index do |item, n|
|
48
|
+
push_margin do
|
49
|
+
render_item(item)
|
50
|
+
end
|
51
|
+
blank_line if (n < section.items.length - 1)
|
52
|
+
end
|
53
|
+
|
54
|
+
section.periods.each_with_index do |period, n|
|
55
|
+
push_margin do
|
56
|
+
render_period period
|
57
|
+
end
|
58
|
+
blank_line if (n < section.periods.length - 1)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def top_line(text)
|
63
|
+
if @opts[:centered_top]
|
64
|
+
center text
|
65
|
+
else
|
66
|
+
add_line text
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def render_period(period)
|
71
|
+
add_line(period.title) if period.title
|
72
|
+
add_line period.line
|
73
|
+
|
74
|
+
if period.has_items?
|
75
|
+
blank_line
|
76
|
+
push_margin do
|
77
|
+
period.items.each_with_index do |item, n|
|
78
|
+
render_item item
|
79
|
+
blank_line if (n < period.items.length - 1)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def render_item(item)
|
86
|
+
add_line "#{@opts[:item_bullet]} " + item.text
|
87
|
+
end
|
88
|
+
|
89
|
+
def render_paragraph(para)
|
90
|
+
push_margin { add_line para }
|
91
|
+
end
|
92
|
+
|
93
|
+
def reset_content
|
94
|
+
@text_content = String.new
|
95
|
+
end
|
96
|
+
|
97
|
+
def content
|
98
|
+
@text_content ||= String.new
|
99
|
+
end
|
100
|
+
|
101
|
+
def center(line)
|
102
|
+
line = @opts[:newline] if line.nil?
|
103
|
+
content << @format.center(line)
|
104
|
+
end
|
105
|
+
|
106
|
+
def add_line(line="")
|
107
|
+
line = @opts[:newline] if line.nil?
|
108
|
+
content << @format.format(line)
|
109
|
+
end
|
110
|
+
|
111
|
+
def blank_line
|
112
|
+
content << @opts[:newline]
|
113
|
+
end
|
114
|
+
|
115
|
+
def push_margin(cols=0, &block)
|
116
|
+
cols = @opts[:indent] if (cols == 0)
|
117
|
+
previous_margin = @format.left_margin
|
118
|
+
@format.left_margin = @format.left_margin + cols
|
119
|
+
block.call
|
120
|
+
@format.left_margin = previous_margin
|
121
|
+
end
|
122
|
+
|
123
|
+
def header(text)
|
124
|
+
add_line(text)
|
125
|
+
add_line(@opts[:header_filler] * text.length) if @opts[:lined_headers]
|
126
|
+
blank_line
|
127
|
+
end
|
128
|
+
end #class PlainText
|
129
|
+
|
130
|
+
module PlainTextMethods
|
131
|
+
# Render to plain text
|
132
|
+
def render_plain_text(opts={})
|
133
|
+
renderer = PlainText.new(self)
|
134
|
+
renderer.render(opts)
|
135
|
+
end
|
136
|
+
end #module Methods
|
137
|
+
|
138
|
+
end #module Renderer
|
139
|
+
|
140
|
+
Resume.class_eval do
|
141
|
+
include Renderer::PlainTextMethods
|
142
|
+
end
|
143
|
+
end #module ResumeTools
|