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
data/lib/eleanor.rb
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
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
|
+
require 'eleanor/length'
|
|
22
|
+
require 'eleanor/parser'
|
|
23
|
+
require 'yaml'
|
|
24
|
+
|
|
25
|
+
module Eleanor
|
|
26
|
+
|
|
27
|
+
# Program name
|
|
28
|
+
NAME= 'Eleanor'
|
|
29
|
+
|
|
30
|
+
# Program version
|
|
31
|
+
VERSION= '1.0.0'
|
|
32
|
+
|
|
33
|
+
# Loads a YAML config file from +filename+. Must be called before anything
|
|
34
|
+
# else is done. This method creates class and instance methods corresponding
|
|
35
|
+
# to the options defined in the YAML. See "Configuration" in the README.
|
|
36
|
+
def self.load_config filename
|
|
37
|
+
config= YAML.load_file(filename)
|
|
38
|
+
Length.line_height= config['Screenplay']['line_height_points']
|
|
39
|
+
config.each_pair do |class_name, hash|
|
|
40
|
+
klass= const_get(class_name)
|
|
41
|
+
hash.each_pair do |att_name, att_val|
|
|
42
|
+
# define a class method that just returns the value of the trait
|
|
43
|
+
(class << klass; self; end).instance_eval do
|
|
44
|
+
define_method(att_name) { att_val }
|
|
45
|
+
end
|
|
46
|
+
# define an instance method that attempts to eval the trait if it's a
|
|
47
|
+
# string and just returns its value otherwise. the method may be passed
|
|
48
|
+
# a hash; each key in the hash is made avaiable to the eval'ed code as a
|
|
49
|
+
# function that returns the key's value.
|
|
50
|
+
klass.class_eval do
|
|
51
|
+
define_method(att_name) do |*args|
|
|
52
|
+
args= args[0] || {}
|
|
53
|
+
if att_val.is_a? String
|
|
54
|
+
closure= self.dup
|
|
55
|
+
(class << closure; self; end).instance_eval do
|
|
56
|
+
args.each_pair do |arg_name, arg_val|
|
|
57
|
+
define_method(arg_name) { arg_val }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
closure.instance_eval do
|
|
61
|
+
begin
|
|
62
|
+
eval(att_val)
|
|
63
|
+
rescue StandardError, SyntaxError
|
|
64
|
+
att_val
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
att_val
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns a new Screenplay created by parsing the plain text screenplay
|
|
77
|
+
# at +filename+. If parsing fails, returns +nil+.
|
|
78
|
+
def self.parse filename
|
|
79
|
+
screenplay= Screenplay.new
|
|
80
|
+
screenplay.parse!(filename) and screenplay or nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Base Paragraph class from which all paragraphs inherit.
|
|
86
|
+
#
|
|
87
|
+
# Note that Eleanor.load_config dynamically adds both class and instance
|
|
88
|
+
# methods to Paragraph and its subclasses depending on the options present in
|
|
89
|
+
# the user's configuration file. See "Configuration" in the README.
|
|
90
|
+
class Paragraph
|
|
91
|
+
|
|
92
|
+
# Set by pagination
|
|
93
|
+
attr_accessor :is_first_on_page
|
|
94
|
+
|
|
95
|
+
# See initialize
|
|
96
|
+
attr_reader :input_line, :last_character_cue, :screenplay, :split_state
|
|
97
|
+
|
|
98
|
+
# Array of lines in the paragraph
|
|
99
|
+
attr_reader :lines
|
|
100
|
+
|
|
101
|
+
# The height (a Length) of the paragraph given its screenplay's line height
|
|
102
|
+
# and the number of lines in the paragraph.
|
|
103
|
+
def height
|
|
104
|
+
@screenplay.line_height * @lines.size
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A paragraph must be attached to a +screenplay+. +input_line+ is the line
|
|
108
|
+
# of raw text used to build the paragraph. +last_character_cue+ should be
|
|
109
|
+
# a reference to the last CharacterCue paragraph seen in the input and is
|
|
110
|
+
# used to keep track of Dialog continuations. +split_state+ should be one
|
|
111
|
+
# of :whole, :orphan, or :widow and indicates if the paragraph is intact on
|
|
112
|
+
# one page, split across pages and at the bottom of the first page, or split
|
|
113
|
+
# across pages and at the top of the second page.
|
|
114
|
+
def initialize screenplay, input_line, last_character_cue, split_state=:whole
|
|
115
|
+
@screenplay= screenplay
|
|
116
|
+
@last_character_cue= last_character_cue
|
|
117
|
+
@split_state= split_state
|
|
118
|
+
@is_first_on_page= false
|
|
119
|
+
@input_line= (self.respond_to?(:text) ? self.text : input_line)
|
|
120
|
+
@fixed_line= fix_input_line
|
|
121
|
+
@sentences= @fixed_line.split(/ {2,}/)
|
|
122
|
+
break_lines!
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Returns true if the paragraph should be kept on the same page as +para+.
|
|
126
|
+
def keep_with_next? para
|
|
127
|
+
if self.class.keep_with_nexts.is_a? Array
|
|
128
|
+
self.class.keep_with_nexts.include? para.class.name[/[^:]+$/]
|
|
129
|
+
else
|
|
130
|
+
self.keep_with_nexts(:next_para => para)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns the Length between the paragraph and +para+.
|
|
135
|
+
def margin_between para
|
|
136
|
+
[self.margin_bottom(:next_para => para),
|
|
137
|
+
para.margin_top(:prev_para => self)].max
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Splits the paragraph across a page break, respecting orphan and widow
|
|
141
|
+
# sentences and lines. Returns [orphan, widow], where orphan is a new
|
|
142
|
+
# paragraph suitable for the bottom of the existing page, and widow is a
|
|
143
|
+
# new paragraph suitable for the top of a new page. orphan's height will
|
|
144
|
+
# be no more than +max_orphan_height+ (a Length). If such constraints cannot
|
|
145
|
+
# be satisfied, the paragraph cannot be broken, and [+nil+, +self+] is
|
|
146
|
+
# returned.
|
|
147
|
+
def split max_orphan_height
|
|
148
|
+
if !self.can_break_across_pages ||
|
|
149
|
+
@sentences.size < (self.min_orphan_sentences_allowed +
|
|
150
|
+
self.min_widow_sentences_allowed)
|
|
151
|
+
return [nil, self]
|
|
152
|
+
end
|
|
153
|
+
# [[orphan_para, widow_para] | orphan_para has
|
|
154
|
+
# self.class.min_orphan_sentences_allowed sentences, ... ]
|
|
155
|
+
# assume self.class.min_orphan_sentences_allowed >= 1
|
|
156
|
+
# assume self.class.min_widow_sentences_allowed >= 1
|
|
157
|
+
lo= self.min_orphan_sentences_allowed - 1
|
|
158
|
+
hi= @sentences.size - self.min_widow_sentences_allowed - 1
|
|
159
|
+
(lo..hi).inject([]) do |paras, i|
|
|
160
|
+
orphan= slice(0..i, :orphan)
|
|
161
|
+
break paras if orphan && orphan.height > max_orphan_height
|
|
162
|
+
widow= slice((i + 1)..-1, :widow)
|
|
163
|
+
paras << [orphan, widow]
|
|
164
|
+
end.reverse_each do |paras|
|
|
165
|
+
if paras[0].lines.size >= self.min_orphan_lines_allowed &&
|
|
166
|
+
paras[1].lines.size >= self.min_widow_lines_allowed
|
|
167
|
+
return paras
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
[nil, self]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def to_s
|
|
174
|
+
"[#{self.class.name[/[^:]+$/]}] #{@input_line}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
# Breaks the fixed input line into an array of lines according to the width
|
|
180
|
+
# of the paragraph.
|
|
181
|
+
def break_lines! hanging_indent=0
|
|
182
|
+
@lines= ['']
|
|
183
|
+
@sentences.each do |sentence|
|
|
184
|
+
sentence.split(/ /).each do |term|
|
|
185
|
+
# term is a single word that either contains hyphens or doesn't
|
|
186
|
+
term.scan(/[^-]+-+|[^-]+|-+/) do |word|
|
|
187
|
+
# word either contains no hyphens, ends in hyphens, or is only
|
|
188
|
+
# hyphens
|
|
189
|
+
width= @screenplay.text_width(@lines.last + word)
|
|
190
|
+
# new line needed
|
|
191
|
+
if width > self.width
|
|
192
|
+
@lines.last.rstrip!
|
|
193
|
+
indent_str= (' ' * hanging_indent)
|
|
194
|
+
# if word is just hyphens, it shouldn't start a line. chop the
|
|
195
|
+
# tail of the last line, move it to the front of the new line.
|
|
196
|
+
if /^-+$/ =~ word
|
|
197
|
+
tail= nil
|
|
198
|
+
@lines.last.sub!(/\s*[^\s]+$/) do |match|
|
|
199
|
+
tail= "#{match} "
|
|
200
|
+
''
|
|
201
|
+
end
|
|
202
|
+
@lines.pop if @lines.last.empty?
|
|
203
|
+
@lines << indent_str + tail.lstrip
|
|
204
|
+
else
|
|
205
|
+
@lines << indent_str
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
@lines.last << word
|
|
209
|
+
end
|
|
210
|
+
@lines.last << ' '
|
|
211
|
+
end
|
|
212
|
+
@lines.last << ' '
|
|
213
|
+
end
|
|
214
|
+
@lines.last.rstrip!
|
|
215
|
+
if self.limit_to_one_line && @lines.size > 1
|
|
216
|
+
raise "pagination error: line too long: attempted to break #{self}"
|
|
217
|
+
end
|
|
218
|
+
@lines
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Some subclasses (e.g., a continued CharacterCue) need to massage the
|
|
222
|
+
# input line in some way.
|
|
223
|
+
def fix_input_line
|
|
224
|
+
@input_line
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Returns a new paragraph whose sentences are those of this paragraph in
|
|
228
|
+
# the range of +sentence_range+. The new paragraph will have the given
|
|
229
|
+
# +split_state+.
|
|
230
|
+
def slice sentence_range, split_state
|
|
231
|
+
self.class.new(@screenplay,
|
|
232
|
+
@sentences[sentence_range].join(' '),
|
|
233
|
+
@last_character_cue,
|
|
234
|
+
@split_state)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
class Action < Paragraph; end
|
|
240
|
+
|
|
241
|
+
class CharacterCue < Paragraph
|
|
242
|
+
|
|
243
|
+
private
|
|
244
|
+
|
|
245
|
+
# Tacks on widow and continuation modifiers as necessary.
|
|
246
|
+
def fix_input_line
|
|
247
|
+
line= @input_line.upcase
|
|
248
|
+
contd_str=
|
|
249
|
+
if @split_state == :widow
|
|
250
|
+
widow_modifier
|
|
251
|
+
elsif @last_character_cue &&
|
|
252
|
+
@last_character_cue.input_line == @input_line
|
|
253
|
+
continuation_modifier
|
|
254
|
+
else
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
257
|
+
if contd_str.nil? || contd_str.empty?
|
|
258
|
+
line
|
|
259
|
+
else
|
|
260
|
+
regex= /\)\s*$/
|
|
261
|
+
if regex =~ line
|
|
262
|
+
line.sub(regex, ", #{contd_str})")
|
|
263
|
+
else
|
|
264
|
+
"#{line} (#{contd_str})"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
class Dialog < Paragraph; end
|
|
272
|
+
|
|
273
|
+
class Insert < Paragraph; end
|
|
274
|
+
|
|
275
|
+
class MontageHeading < Paragraph; end
|
|
276
|
+
|
|
277
|
+
class MontageItem < Paragraph
|
|
278
|
+
private
|
|
279
|
+
def break_lines!
|
|
280
|
+
super(3)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# When a Dialog is split across two pages, a More paragraph will be added as
|
|
285
|
+
# the last paragraph on the first page.
|
|
286
|
+
class More < Paragraph
|
|
287
|
+
def initialize screenplay
|
|
288
|
+
super(screenplay, nil, nil, :whole)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
class Parenthetical < Paragraph
|
|
293
|
+
private
|
|
294
|
+
def break_lines!
|
|
295
|
+
super(1)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
class SceneHeading < Paragraph
|
|
300
|
+
private
|
|
301
|
+
def fix_input_line
|
|
302
|
+
@input_line.upcase
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
class SlugLine < Paragraph; end
|
|
307
|
+
|
|
308
|
+
class Transition < Paragraph; end
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# Encapsulates paragraphs.
|
|
313
|
+
#
|
|
314
|
+
# Note that Eleanor.load_config dynamically adds both class and instance
|
|
315
|
+
# methods to Page and its subclasses depending on the options present in the
|
|
316
|
+
# user's configuration file. See "Configuration" in the README.
|
|
317
|
+
class Page
|
|
318
|
+
|
|
319
|
+
# See initialize
|
|
320
|
+
attr_reader :page_no, :screenplay
|
|
321
|
+
|
|
322
|
+
# Array of Paragraphs
|
|
323
|
+
attr_reader :paras
|
|
324
|
+
|
|
325
|
+
# The page's body is all the paragraphs (and the margins between them)
|
|
326
|
+
# between the page's top and bottom margins. Returns a Length.
|
|
327
|
+
def body_height
|
|
328
|
+
prev_para= nil
|
|
329
|
+
@paras.inject(0.points) do |total, para|
|
|
330
|
+
prev, prev_para= [prev_para, para]
|
|
331
|
+
total + para.height + (prev.nil?? 0.points : prev.margin_between(para))
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Pages must be attached to a +screenplay+. +page_no+ is the page number.
|
|
336
|
+
def initialize screenplay, page_no
|
|
337
|
+
@screenplay= screenplay
|
|
338
|
+
@page_no= page_no
|
|
339
|
+
@paras= []
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# The page's top margin plus the top margin of the first paragraph on the
|
|
343
|
+
# page. A Length.
|
|
344
|
+
def margin_top_actual
|
|
345
|
+
self.margin_top + (@paras.empty?? 0.points : @paras.first.margin_top)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# The page's maximum body height (see body_height) given its top and bottom
|
|
349
|
+
# margins.
|
|
350
|
+
def max_body_height
|
|
351
|
+
self.height - self.margin_top_actual - self.margin_bottom
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Attempts to add +para+ to the end of the page. If +para+ is split by the
|
|
355
|
+
# page break as a result or cannot be added at all, returns a new Paragraph
|
|
356
|
+
# which should be added to the top of a new page. Otherwise, if +para+ fits
|
|
357
|
+
# on the page, +nil+ is returned. If +force+ is true +para+ is added to the
|
|
358
|
+
# page regardless of constraints.
|
|
359
|
+
def push_para para, force=false
|
|
360
|
+
prev_para= @paras.last
|
|
361
|
+
if prev_para.nil? && (para.is_a?(Dialog) || para.is_a?(Parenthetical))
|
|
362
|
+
prev_para= CharacterCue.new(para.screenplay,
|
|
363
|
+
para.last_character_cue.input_line,
|
|
364
|
+
nil,
|
|
365
|
+
:widow)
|
|
366
|
+
self.push_para(prev_para)
|
|
367
|
+
end
|
|
368
|
+
margin_between= (prev_para.nil?? 0.points : prev_para.margin_between(para))
|
|
369
|
+
height_before_para= self.body_height + margin_between
|
|
370
|
+
# para overruns the current page. need to start a new page.
|
|
371
|
+
curr_page_para, new_page_para=
|
|
372
|
+
if (height_before_para + para.height > self.max_body_height) && !force
|
|
373
|
+
if height_before_para >= self.max_body_height
|
|
374
|
+
[nil, para]
|
|
375
|
+
else
|
|
376
|
+
orphan, widow= para.split(self.max_body_height - height_before_para)
|
|
377
|
+
if orphan.nil?
|
|
378
|
+
[nil, para]
|
|
379
|
+
else
|
|
380
|
+
[orphan, widow]
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
else
|
|
384
|
+
[para, nil]
|
|
385
|
+
end
|
|
386
|
+
unless curr_page_para.nil?
|
|
387
|
+
curr_page_para.is_first_on_page= true if @paras.empty?
|
|
388
|
+
@paras << curr_page_para
|
|
389
|
+
end
|
|
390
|
+
new_page_para
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def to_s
|
|
394
|
+
str= '- ' + @page_no.to_s + ' ' + ('-' * 72) + "\n"
|
|
395
|
+
@paras.inject(str) { |s, para| "#{s}#{para}\n"}
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# Encapsulates paragraphs and pages.
|
|
403
|
+
#
|
|
404
|
+
# Note that Eleanor.load_config dynamically adds both class and instance
|
|
405
|
+
# methods to Screenplay depending on the options present in the user's
|
|
406
|
+
# configuration file. See "Configuration" in the README.
|
|
407
|
+
class Screenplay
|
|
408
|
+
|
|
409
|
+
# Array of Page objects. Filled in during pagination.
|
|
410
|
+
attr_reader :pages
|
|
411
|
+
|
|
412
|
+
# Array of Paragraph objects. Filled in during parsing, used during
|
|
413
|
+
# pagination.
|
|
414
|
+
attr_reader :paras
|
|
415
|
+
|
|
416
|
+
# Array of TitlePage objects. Filled in during parsing.
|
|
417
|
+
attr_reader :title_pages
|
|
418
|
+
|
|
419
|
+
# The screenplay's author, nil if none.
|
|
420
|
+
def author
|
|
421
|
+
@title_pages.each { |tp| tp.author and return tp.author }
|
|
422
|
+
nil
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def initialize
|
|
426
|
+
@paras= []
|
|
427
|
+
@title_pages= []
|
|
428
|
+
@pages= [Page.new(self, 1)]
|
|
429
|
+
initialize_paper!
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Paginates the screenplay according to its and all the constraints of its
|
|
433
|
+
# paragraphs.
|
|
434
|
+
def paginate!
|
|
435
|
+
@paras.each do |para|
|
|
436
|
+
new_para= @pages.last.push_para(para)
|
|
437
|
+
unless new_para.nil?
|
|
438
|
+
new_paras= [new_para]
|
|
439
|
+
if new_para.split_state == :whole
|
|
440
|
+
while @pages.last.paras.last.keep_with_next? new_paras.first
|
|
441
|
+
new_paras.unshift(@pages.last.paras.pop)
|
|
442
|
+
if @pages.last.paras.empty?
|
|
443
|
+
raise "pagination error: string of keep-with-nexts larger " \
|
|
444
|
+
"than one page; occured at:\n #{para}"
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
new_page= Page.new(self, @pages.size + 1)
|
|
449
|
+
new_paras.each do |p|
|
|
450
|
+
new_page_para= new_page.push_para(p)
|
|
451
|
+
if new_page_para
|
|
452
|
+
raise "pagination error: string of keep-with-nexts larger than " \
|
|
453
|
+
"one page:\n #{new_page_para}"
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
first_para= new_page.paras.first
|
|
457
|
+
if first_para.is_a?(CharacterCue) && first_para.split_state == :widow
|
|
458
|
+
@pages.last.push_para(More.new(self), true)
|
|
459
|
+
end
|
|
460
|
+
@pages << new_page
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
@pages
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Parses the plain text screenplay at +filename+. Returns +nil+ if parsing
|
|
467
|
+
# failed and an array of paragraphs if it succeeded. The paras attribute
|
|
468
|
+
# will be valid if parsing is successful.
|
|
469
|
+
def parse! filename
|
|
470
|
+
parse_! filename
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# The screenplay's title, nil if none.
|
|
474
|
+
def title
|
|
475
|
+
@title_pages.each { |tp| tp.title and return tp.title }
|
|
476
|
+
nil
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def to_s paginated=true
|
|
480
|
+
if paginated
|
|
481
|
+
@pages.inject('') { |str, page| "#{str}#{page}" }
|
|
482
|
+
else
|
|
483
|
+
@paras.inject('') { |str, para| "#{str}#{para}\n" }
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
private :parse_!
|
|
488
|
+
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
# A screenplay title page.
|
|
494
|
+
class TitlePage < Page
|
|
495
|
+
|
|
496
|
+
# See initialize
|
|
497
|
+
attr_reader :author, :contact, :title
|
|
498
|
+
|
|
499
|
+
# +options+ can include any title page elements recognized by the parser
|
|
500
|
+
# and backend. With the default parser and backend, options include
|
|
501
|
+
# :author, :contact, and :title. :author and :title are strings. :contact
|
|
502
|
+
# is an array of strings. Each option is turned into an instance variable.
|
|
503
|
+
def initialize options={}
|
|
504
|
+
options.each_pair do |att_name, att_val|
|
|
505
|
+
instance_variable_set("@#{att_name}", att_val)
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
end
|