eleanor 1.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.
- data/CHANGELOG +3 -0
- data/INSTALL +43 -0
- data/LICENSE +19 -0
- data/README +439 -0
- data/Rakefile +88 -0
- data/bin/eleanor +127 -0
- data/data/eleanor/eleanor.yaml +165 -0
- data/examples/example.txt +167 -0
- data/lib/eleanor.rb +511 -0
- data/lib/eleanor/hpdfpaper.rb +328 -0
- data/lib/eleanor/length.rb +172 -0
- data/lib/eleanor/parser.rb +1255 -0
- data/setup.rb +1585 -0
- data/src/ragel/parser.rl +166 -0
- metadata +81 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# Copyright (c) 2008 chiisaitsu <chiisaitsu@gmail.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
# THE SOFTWARE.
|
|
20
|
+
#
|
|
21
|
+
# ---
|
|
22
|
+
#
|
|
23
|
+
# This is Eleanor's backend. Eleanor uses a paper metaphor to talk to the
|
|
24
|
+
# backend: after parsing and pagination, Eleanor "writes" the screenplay to
|
|
25
|
+
# "paper," which by default is PDF. But you could write a backend to target
|
|
26
|
+
# anything, like XML, XSL-FO, Postscript, RTF, the screen, a socket, whatever.
|
|
27
|
+
# Depending on the capabilities of the paper, a backend may choose to ignore
|
|
28
|
+
# some of the user's configuration options, like font, line height, and character
|
|
29
|
+
# spacing.
|
|
30
|
+
#
|
|
31
|
+
# Eleanor's interface with the backend is really simple, allowing wide latitude
|
|
32
|
+
# in how it's implemented. No special modules or classes are needed. Just add
|
|
33
|
+
# five instance methods to the Screenplay class, and Eleanor will call them when
|
|
34
|
+
# appropriate. (You'll probably end up adding other methods to other classes in
|
|
35
|
+
# your implementation, too.)
|
|
36
|
+
#
|
|
37
|
+
# [initialize_paper!]
|
|
38
|
+
# Called when the screenplay is initialized. Can be used to setup any
|
|
39
|
+
# instance variables needed by the backend.
|
|
40
|
+
# [line_height]
|
|
41
|
+
# Called when calculating the height of a paragraph. Returns a Length. If the
|
|
42
|
+
# paper supports custom line heights, this method should make use of
|
|
43
|
+
# Screenplay#line_height_points, which returns the value specified in the
|
|
44
|
+
# user's configuration YAML.
|
|
45
|
+
# [save_paper(out_filename, in_filename=nil)]
|
|
46
|
+
# Writes out the paper representation to +out_filename+. If +out_filename+ is
|
|
47
|
+
# nil, the method may use +in_filename+ to generate an output filename. For
|
|
48
|
+
# example, if +in_filename+ is "screenplay.txt", the method might return
|
|
49
|
+
# "screenplay.pdf".
|
|
50
|
+
# [text_width(str)]
|
|
51
|
+
# Returns the width (a Length) of +str+ in the current font size and character
|
|
52
|
+
# spacing. If the paper supports custom font sizes and character spacing,
|
|
53
|
+
# this method should make use of Screenplay#font_size and
|
|
54
|
+
# Screenplay#char_spacing, which return the values specified in the user's
|
|
55
|
+
# configuration YAML.
|
|
56
|
+
# [write_to_paper!]
|
|
57
|
+
# Called after parsing and pagination. Translates the screenplay to the paper
|
|
58
|
+
# representation.
|
|
59
|
+
#
|
|
60
|
+
# This implementation uses libHaru[http://libharu.org/], a free and open-source
|
|
61
|
+
# PDF library written in ANSI C that comes with Ruby bindings. It allows text
|
|
62
|
+
# underlining by surrounding bits of text in underscores:
|
|
63
|
+
#
|
|
64
|
+
# You can underline a single _word_, or _many words at once._
|
|
65
|
+
|
|
66
|
+
require 'hpdf'
|
|
67
|
+
|
|
68
|
+
# Some handy, high-level methods for libHaru's HPDFPage class.
|
|
69
|
+
class HPDFPage
|
|
70
|
+
|
|
71
|
+
alias :begin_text_ :begin_text
|
|
72
|
+
alias :end_text_ :end_text
|
|
73
|
+
alias :text_width_ :text_width
|
|
74
|
+
|
|
75
|
+
# Overridden to implement underlining.
|
|
76
|
+
def begin_text
|
|
77
|
+
self.begin_text_
|
|
78
|
+
@underline_coords= []
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Overridden to implement underlining.
|
|
82
|
+
def end_text
|
|
83
|
+
self.end_text_
|
|
84
|
+
@underline_coords.each do |pair|
|
|
85
|
+
self.move_to(pair[0][0], pair[0][1])
|
|
86
|
+
self.line_to(pair[1][0], pair[1][1])
|
|
87
|
+
self.stroke
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Writes a line, +str+, to the page and moves the text pointer down the page
|
|
92
|
+
# by +line_height_pts+ points. Any text surrounded by underscores is
|
|
93
|
+
# underlined.
|
|
94
|
+
def line str, line_height_pts
|
|
95
|
+
unless str.nil? || str.empty?
|
|
96
|
+
pos= self.get_current_text_pos
|
|
97
|
+
# implement underlining: split the line on "_" => every other segment is a
|
|
98
|
+
# string of text that should be underlined.
|
|
99
|
+
segs= str.split(/_/)
|
|
100
|
+
segs.each_with_index do |seg, si|
|
|
101
|
+
if si % 2 == 1
|
|
102
|
+
y= pos[1] - 1 # draw underline one point below line
|
|
103
|
+
coord1= [pos[0] + self.text_width(segs[0..(si - 1)].join), y]
|
|
104
|
+
coord2= [coord1[0] + self.text_width(seg), y]
|
|
105
|
+
@underline_coords << [coord1, coord2]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
# finally, print the line
|
|
109
|
+
self.show_text(segs.join)
|
|
110
|
+
end
|
|
111
|
+
self.move_text_pos(0, -line_height_pts)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Writes a centered line at the current vertical position. See line.
|
|
115
|
+
def line_center str, line_height_pts
|
|
116
|
+
self.move_text_pos(-self.get_current_text_pos[0] +
|
|
117
|
+
(self.get_width / 2.0) -
|
|
118
|
+
(self.text_width(str) / 2.0),
|
|
119
|
+
0)
|
|
120
|
+
self.line(str, line_height_pts)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Writes a line flushed to the right margin, which is specified by
|
|
124
|
+
# +margin_right+, a Length, at the current vertical position. See line.
|
|
125
|
+
def line_flush_right str, line_height_pts, margin_right
|
|
126
|
+
self.move_text_pos(-self.get_current_text_pos[0] +
|
|
127
|
+
self.get_width -
|
|
128
|
+
(margin_right.to_points.to_f) -
|
|
129
|
+
self.text_width(str),
|
|
130
|
+
0)
|
|
131
|
+
self.line(str, line_height_pts)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Moves the text pointer horizontally to the given margin +length+.
|
|
135
|
+
def margin_left= length
|
|
136
|
+
self.move_text_pos(-self.get_current_text_pos[0] + length.to_points.to_f, 0)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Moves the text pointer down the page by +length+.
|
|
140
|
+
def move_down length
|
|
141
|
+
self.move_text_pos(0, -length.to_points.to_f)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Moves the text pointer to the very first line on the page.
|
|
145
|
+
def move_to_top
|
|
146
|
+
# + 3 because without it, there's a little gap at the top
|
|
147
|
+
self.move_text_pos(0,
|
|
148
|
+
-self.get_current_text_pos[1] +
|
|
149
|
+
self.get_height -
|
|
150
|
+
self.get_current_font_size + 3)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Overridden to implement underlining.
|
|
154
|
+
def text_width str
|
|
155
|
+
self.text_width_(str.gsub(/_/, ''))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
module Eleanor
|
|
163
|
+
|
|
164
|
+
class Page
|
|
165
|
+
|
|
166
|
+
# An implementation detail in the backend. See lib/eleanor/hpdfpaper.rb.
|
|
167
|
+
def write_to_paper pdf_page, line_height_pts
|
|
168
|
+
pdf_page.begin_text
|
|
169
|
+
# header
|
|
170
|
+
if self.header
|
|
171
|
+
pdf_page.move_to_top
|
|
172
|
+
pdf_page.move_down(self.header_margin_top)
|
|
173
|
+
pdf_page.line_center(self.header, line_height_pts)
|
|
174
|
+
end
|
|
175
|
+
# page number
|
|
176
|
+
if self.page_number_display
|
|
177
|
+
pdf_page.move_to_top
|
|
178
|
+
pdf_page.move_down(self.page_number_margin_top)
|
|
179
|
+
pdf_page.line_flush_right(self.page_number_display,
|
|
180
|
+
line_height_pts,
|
|
181
|
+
self.page_number_margin_right)
|
|
182
|
+
end
|
|
183
|
+
# finally, paragraphs
|
|
184
|
+
pdf_page.move_to_top
|
|
185
|
+
pdf_page.move_down(self.margin_top_actual)
|
|
186
|
+
prev_para= nil
|
|
187
|
+
@paras.each do |para|
|
|
188
|
+
pdf_page.move_down(prev_para.margin_between(para)) if prev_para
|
|
189
|
+
para.write_to_paper(pdf_page, line_height_pts)
|
|
190
|
+
prev_para= para
|
|
191
|
+
end
|
|
192
|
+
pdf_page.end_text
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class Paragraph
|
|
200
|
+
|
|
201
|
+
# An implementation detail in the backend. See lib/eleanor/hpdfpaper.rb.
|
|
202
|
+
def write_to_paper pdf_page, line_height_pts
|
|
203
|
+
underline_broken= false
|
|
204
|
+
@lines.each do |line|
|
|
205
|
+
if underline_broken
|
|
206
|
+
line= '_' + line
|
|
207
|
+
underline_broken= false
|
|
208
|
+
end
|
|
209
|
+
if line.count('_') % 2 == 1
|
|
210
|
+
line << '_'
|
|
211
|
+
underline_broken= true
|
|
212
|
+
end
|
|
213
|
+
case self.align.to_s.strip.downcase
|
|
214
|
+
when 'left'
|
|
215
|
+
pdf_page.margin_left= self.margin_left
|
|
216
|
+
pdf_page.line(line, line_height_pts)
|
|
217
|
+
when 'center'
|
|
218
|
+
pdf_page.line_center(line, line_height_pts)
|
|
219
|
+
when 'right'
|
|
220
|
+
pdf_page.line_flush_right(line, line_height_pts, self.margin_right)
|
|
221
|
+
else
|
|
222
|
+
raise "configuration error: invalid align value " \
|
|
223
|
+
"#{self.align.inspect} for #{self.class}"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
if underline_broken
|
|
227
|
+
warn "warning: runaway underline at paragraph:\n #{self}"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class Screenplay
|
|
236
|
+
|
|
237
|
+
# A required method in the backend. See lib/eleanor/hpdfpaper.rb.
|
|
238
|
+
def initialize_paper!
|
|
239
|
+
@pdf= HPDFDoc.new
|
|
240
|
+
font_name= (%r{[\./\\]} =~ self.font ?
|
|
241
|
+
@pdf.load_ttfont_from_file(self.font, HPDFDoc::HPDF_TRUE) :
|
|
242
|
+
self.font)
|
|
243
|
+
@pdf_font= @pdf.get_font(font_name, nil)
|
|
244
|
+
@first_pdf_page= add_pdf_page(@pages.first)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# A required method in the backend. See lib/eleanor/hpdfpaper.rb.
|
|
248
|
+
def line_height
|
|
249
|
+
self.line_height_points.points
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# A required method in the backend. See lib/eleanor/hpdfpaper.rb.
|
|
253
|
+
def save_paper out_filename, in_filename=nil
|
|
254
|
+
out_filename ||= (File.join(File.dirname(in_filename),
|
|
255
|
+
File.basename(in_filename,
|
|
256
|
+
File.extname(in_filename))) +
|
|
257
|
+
'.pdf')
|
|
258
|
+
@pdf.save_to_file(out_filename)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# A required method in the backend. See lib/eleanor/hpdfpaper.rb.
|
|
262
|
+
def text_width str
|
|
263
|
+
@first_pdf_page.text_width(str).points
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# A required method in the backend. See lib/eleanor/hpdfpaper.rb.
|
|
267
|
+
def write_to_paper!
|
|
268
|
+
if self.title
|
|
269
|
+
@pdf.set_info_attr(HPDFDoc::HPDF_INFO_TITLE, self.title.gsub(/_/, ''))
|
|
270
|
+
end
|
|
271
|
+
@pdf.set_info_attr(HPDFDoc::HPDF_INFO_AUTHOR, self.author) if self.author
|
|
272
|
+
@pdf.set_info_attr(HPDFDoc::HPDF_INFO_CREATOR,
|
|
273
|
+
"#{Eleanor::NAME} #{Eleanor::VERSION}")
|
|
274
|
+
@title_pages.each do |page|
|
|
275
|
+
pdf_page= add_pdf_page(page, @first_pdf_page)
|
|
276
|
+
page.write_to_paper(pdf_page, self.line_height_points)
|
|
277
|
+
end
|
|
278
|
+
@pages.each_with_index do |page, i|
|
|
279
|
+
pdf_page= (i == 0 ? @first_pdf_page : add_pdf_page(page))
|
|
280
|
+
page.write_to_paper(pdf_page, self.line_height_points)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
private
|
|
285
|
+
|
|
286
|
+
def add_pdf_page eleanor_page, before_pdf_page=nil
|
|
287
|
+
pdf_page= (before_pdf_page.nil??
|
|
288
|
+
@pdf.add_page :
|
|
289
|
+
@pdf.insert_page(before_pdf_page))
|
|
290
|
+
pdf_page.set_width(eleanor_page.width.to_points.to_f)
|
|
291
|
+
pdf_page.set_height(eleanor_page.height.to_points.to_f)
|
|
292
|
+
pdf_page.set_line_width(0.75) # underline stroke width
|
|
293
|
+
pdf_page.set_font_and_size(@pdf_font, self.font_size.to_points.to_f)
|
|
294
|
+
pdf_page.set_char_space(self.char_spacing.to_points.to_f)
|
|
295
|
+
pdf_page
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class TitlePage
|
|
303
|
+
|
|
304
|
+
# An implementation detail in the backend. See lib/eleanor/hpdfpaper.rb.
|
|
305
|
+
def write_to_paper pdf_page, line_height_pts
|
|
306
|
+
pdf_page.begin_text
|
|
307
|
+
pdf_page.move_to_top
|
|
308
|
+
pdf_page.move_down(self.margin_top)
|
|
309
|
+
pdf_page.line_center(@title, line_height_pts) unless @title.nil?
|
|
310
|
+
pdf_page.move_down(1.lines)
|
|
311
|
+
unless @by.nil?
|
|
312
|
+
pdf_page.line_center(@by, line_height_pts)
|
|
313
|
+
pdf_page.move_down(1.lines)
|
|
314
|
+
end
|
|
315
|
+
pdf_page.line_center(@author, line_height_pts) unless @author.nil?
|
|
316
|
+
unless @contact.nil?
|
|
317
|
+
pdf_page.move_to_top
|
|
318
|
+
pdf_page.move_down(self.height -
|
|
319
|
+
(@contact.size * line_height_pts).points -
|
|
320
|
+
self.margin_bottom)
|
|
321
|
+
pdf_page.margin_left= self.margin_left
|
|
322
|
+
@contact.each { |line| pdf_page.line(line, line_height_pts) }
|
|
323
|
+
end
|
|
324
|
+
pdf_page.end_text
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Copyright (c) 2008 chiisaitsu <chiisaitsu@gmail.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
# THE SOFTWARE.
|
|
20
|
+
#
|
|
21
|
+
# ---
|
|
22
|
+
#
|
|
23
|
+
# See Length class.
|
|
24
|
+
|
|
25
|
+
# A handy way to pass around lengths of various units and mix and match them
|
|
26
|
+
# regardless of unit. Could be a little more robust but works well enough.
|
|
27
|
+
#
|
|
28
|
+
# Units are defined by adding entries to the UNITS hash. That's it. By way of
|
|
29
|
+
# some metaprogramming, each entry in the hash creates a new subclass of
|
|
30
|
+
# Length that shares the name of the entry's key. So, currently there are
|
|
31
|
+
# three Length subclasses: Inches, Lines, and Points.
|
|
32
|
+
#
|
|
33
|
+
# Length.line_height must be set before anything is done with any Lengths. Set
|
|
34
|
+
# it to a value in points:
|
|
35
|
+
#
|
|
36
|
+
# Length.line_height= 12 # OK, now go for it
|
|
37
|
+
#
|
|
38
|
+
# New Length subclass instances can be created in three ways:
|
|
39
|
+
#
|
|
40
|
+
# * Constructors: Points.new(32)
|
|
41
|
+
# * Length conversion instance methods: Inches.new(0.5).to_points
|
|
42
|
+
# * Numeric instance methods: 32.points
|
|
43
|
+
#
|
|
44
|
+
# To get at the raw Numeric that Lengths encapsulate, use to_i or to_f:
|
|
45
|
+
#
|
|
46
|
+
# 1.5.inches.to_i # => 1
|
|
47
|
+
# 1.5.inches.to_f # => 1.5
|
|
48
|
+
#
|
|
49
|
+
# All the operations that apply to Numerics can be applied to Lengths, too:
|
|
50
|
+
#
|
|
51
|
+
# 1.inches + 12.lines # => #<Inches:0x7fed2694 @val=3.0>
|
|
52
|
+
# 12.lines + 1.inches # => #<Lines:0x7fece364 @val=18.0>
|
|
53
|
+
# 2.points * 100 # => #<Points:0x7feca278 @val=200.0>
|
|
54
|
+
# 72.points == 1.inches # => true
|
|
55
|
+
# 72.points == 1 # error!
|
|
56
|
+
#
|
|
57
|
+
# More examples:
|
|
58
|
+
#
|
|
59
|
+
# Length.line_height= 12 # => 12
|
|
60
|
+
# Inches.new(1) # => #<Inches:0x7ff37120 @val=1.0>
|
|
61
|
+
# Inches.new(1).to_points # => #<Points:0x7ff01ebc @val=72.0>
|
|
62
|
+
# 6.lines # => #<Lines:0x7fefde48 @val=6.0>
|
|
63
|
+
# 6.lines.to_points.to_f # => 72
|
|
64
|
+
# 6.lines + 0.5.inches # => #<Lines:0x7fef44ec @val=9.0>
|
|
65
|
+
# Inches.new(72.points) # => #<Inches:0x7ff95ae0 @val=1.0>
|
|
66
|
+
|
|
67
|
+
class Length
|
|
68
|
+
|
|
69
|
+
# All units are defined in terms of points.
|
|
70
|
+
UNITS= {
|
|
71
|
+
:Inches => { :points => 72.0, :abbrev => 'in' },
|
|
72
|
+
:Lines => { :points => nil, :abbrev => 'ln' },
|
|
73
|
+
:Points => { :points => 1.0, :abbrev => 'pt' }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
include Comparable
|
|
77
|
+
|
|
78
|
+
# This is filled in once line_height is set. Maps the abbreviations in
|
|
79
|
+
# the UNITS hash to their corresponding Length subclasses, e.g.,
|
|
80
|
+
# 'in' => Inches.
|
|
81
|
+
ABBREVIATIONS= {}
|
|
82
|
+
|
|
83
|
+
# Sets the line height to +points+. Since calling this kicks off the
|
|
84
|
+
# metaprogramming that builds the Length subclasses, line height must be set
|
|
85
|
+
# before anything else is done.
|
|
86
|
+
def self.line_height= points
|
|
87
|
+
UNITS[:Lines][:points]= points
|
|
88
|
+
UNITS.each_pair do |this_unit, this_meta|
|
|
89
|
+
# create new class this_unit
|
|
90
|
+
klass= Class.new(self)
|
|
91
|
+
# class gets to_<length> methods for each class of length
|
|
92
|
+
klass.class_eval do
|
|
93
|
+
UNITS.each_pair do |unit, meta|
|
|
94
|
+
define_method("to_#{unit.to_s.downcase}") do
|
|
95
|
+
points= (@val * this_meta[:points]) / meta[:points]
|
|
96
|
+
Object.const_get(unit).new(points)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
Object.const_set(this_unit, klass)
|
|
101
|
+
# add entry to abbreviations table
|
|
102
|
+
ABBREVIATIONS[this_meta[:abbrev]]= klass
|
|
103
|
+
# add <this_unit> method to Numeric
|
|
104
|
+
Numeric.class_eval do
|
|
105
|
+
define_method(this_unit.to_s.downcase) { klass.new(self) }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Returns the line height.
|
|
111
|
+
def self.line_height
|
|
112
|
+
UNITS[:Lines][:points]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# If +str+ represents a Length, returns it. Returns nil otherwise. Strings
|
|
116
|
+
# that represent Lengths end in a Length abbreviation, e.g., "32in", "-1.72pt".
|
|
117
|
+
def self.parse str
|
|
118
|
+
match= /[a-zA-Z]+$/.match(str)
|
|
119
|
+
if match.nil? || !ABBREVIATIONS.has_key?(match[0])
|
|
120
|
+
nil
|
|
121
|
+
else
|
|
122
|
+
ABBREVIATIONS[match[0]].new(str[0..-(match[0].length + 1)].to_f)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# +obj+ must be a Length.
|
|
127
|
+
def <=> obj
|
|
128
|
+
if obj.is_a? Length
|
|
129
|
+
@val <=> obj.send("to_#{self.class.name.downcase}").to_f
|
|
130
|
+
else
|
|
131
|
+
raise TypeError, "#{self.class} is incomparable to #{obj.class}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# +val+ may be a Numeric, a String that can be coerced into a Numeric, or
|
|
136
|
+
# a Length.
|
|
137
|
+
def initialize val
|
|
138
|
+
@val=
|
|
139
|
+
case val
|
|
140
|
+
when Numeric, String
|
|
141
|
+
val.to_f
|
|
142
|
+
when Length
|
|
143
|
+
val.send("to_#{self.class.name.downcase}").to_f
|
|
144
|
+
else
|
|
145
|
+
raise TypeError, "cannot initialize #{self.class} from #{val.class}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def to_i
|
|
150
|
+
@val.to_i
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def to_f
|
|
154
|
+
@val
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def to_s
|
|
158
|
+
"#{@val.to_s} #{self.class.name.downcase}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def method_missing meth, *args
|
|
164
|
+
if @val.respond_to? meth
|
|
165
|
+
args= args.map { |a| a.is_a?(Length) ? self.class.new(a).to_f : a }
|
|
166
|
+
self.class.new(@val.send(meth, *args))
|
|
167
|
+
else
|
|
168
|
+
super(meth, args)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
end
|