bible 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/README.txt +33 -0
- data/Rakefile +41 -0
- data/bin/bible.rb +154 -0
- data/lib/bible.rb +33 -0
- data/lib/bible/bible.yml +1717 -0
- data/lib/bible/iterator.rb +80 -0
- data/lib/bible/lookup/dr.rb +42 -0
- data/lib/bible/lookup/nab.rb +40 -0
- data/lib/bible/lookup/rsv.rb +48 -0
- data/lib/bible/parser.rb +905 -0
- data/test/test_bible.rb +449 -0
- metadata +78 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
|
|
2
|
+
module Bible
|
|
3
|
+
|
|
4
|
+
# Will iterate over verses in a given reference, using the lookup given.
|
|
5
|
+
class BibleIterator
|
|
6
|
+
include Enumerable
|
|
7
|
+
attr_reader :reference
|
|
8
|
+
|
|
9
|
+
# default lookup, if none selected
|
|
10
|
+
def self.default_lookup
|
|
11
|
+
@@default_lookup ||= nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.default_lookup=(value)
|
|
15
|
+
@@default_lookup = value
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(ref, lookup = nil)
|
|
19
|
+
raise "A lookup class must be provided if no default is set" if lookup.nil? && @@default_lookup.nil?
|
|
20
|
+
raise "A reference to look up must be provided" if ref.nil?
|
|
21
|
+
@reference = ref
|
|
22
|
+
@lookup = lookup || @@default_lookup
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def method_missing(sym, *args)
|
|
26
|
+
@reference.__send__(sym, *args)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def iterate_chapter(chapter, &blk)
|
|
30
|
+
if chapter.single_verse?
|
|
31
|
+
blk.call(@lookup.get_ref(chapter.book.book_symbol, chapter.chapter_number, chapter.verse.verse_number), chapter.book.book_symbol, chapter.chapter_number, chapter.verse.verse_number)
|
|
32
|
+
else
|
|
33
|
+
chapter.verses.each { |v| blk.call(@lookup.get_ref(chapter.book.book_symbol, chapter.chapter_number, v.verse_number), chapter.book.book_symbol, chapter.chapter_number, v.verse_number) }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def iterate_book(book, &blk)
|
|
38
|
+
if book.single_chapter?
|
|
39
|
+
iterate_chapter(book.chapter, &blk)
|
|
40
|
+
else
|
|
41
|
+
book.chapters.each { |c|
|
|
42
|
+
iterate_chapter(c, &blk)
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def each(&blk)
|
|
48
|
+
if @reference.single_book?
|
|
49
|
+
iterate_book(@reference.book, &blk)
|
|
50
|
+
else
|
|
51
|
+
@reference.books.each { |b| iterate_book(b, &blk) }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_s
|
|
56
|
+
s = ""
|
|
57
|
+
v, b, c = nil, nil, nil
|
|
58
|
+
|
|
59
|
+
self.each do |text, book_symbol, chapter_number, verse_number|
|
|
60
|
+
if book_symbol != b && chapter_number != c
|
|
61
|
+
s << "\n\n" unless s.nil? || s.strip == ""
|
|
62
|
+
s << book_symbol.to_s << ", Chapter " << chapter_number.to_s << "\n\n"
|
|
63
|
+
elsif chapter_number != c
|
|
64
|
+
s << "\n\n" unless s.nil? || s.strip == ""
|
|
65
|
+
s << "Chapter " << chapter_number.to_s << "\n\n"
|
|
66
|
+
elsif (! v.nil?) && v != verse_number - 1
|
|
67
|
+
s << "\n\n"
|
|
68
|
+
end
|
|
69
|
+
b, c, v = book_symbol, chapter_number, verse_number
|
|
70
|
+
s << text
|
|
71
|
+
end
|
|
72
|
+
s
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def inspect
|
|
76
|
+
self.to_s
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'open-uri'
|
|
2
|
+
|
|
3
|
+
module Bible
|
|
4
|
+
class DRLookup
|
|
5
|
+
|
|
6
|
+
@@books = {}
|
|
7
|
+
|
|
8
|
+
def self.getURL(book, chapter)
|
|
9
|
+
raise "chapter cannot be nil" if chapter.nil?
|
|
10
|
+
|
|
11
|
+
offset = Bible::BookInfo.all_books.index(book) + 1
|
|
12
|
+
"http://www.biblegateway.com/passage/?book_id=#{offset}&chapter=#{chapter}&version=63"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.get_ref(book, chapter = nil, verse = nil)
|
|
16
|
+
# TODO: Handle nil chapter
|
|
17
|
+
text = ((@@books[book] ||= {})[chapter] ||= open(getURL(book, chapter)).gets(nil))
|
|
18
|
+
scanner = StringScanner.new(text)
|
|
19
|
+
if ! scanner.skip_until(/<span id="en-DRA-(.*?)".*?>.*?<\/span>/i)
|
|
20
|
+
scanner = StringScanner.new("")
|
|
21
|
+
else
|
|
22
|
+
unless verse.nil?
|
|
23
|
+
if verse > 1
|
|
24
|
+
id = scanner[1].to_i + (verse - 1)
|
|
25
|
+
if ! scanner.skip_until(/<span id="en-DRA-(#{id})".*?>.*?<\/span>/i)
|
|
26
|
+
scanner = StringScanner.new("")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Extract verse and place in string
|
|
31
|
+
scanner = StringScanner.new(scanner.check_until(/<p \/>/)) unless scanner.empty?
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if scanner.empty?
|
|
36
|
+
""
|
|
37
|
+
else
|
|
38
|
+
scanner.rest.strip.gsub(/(\d+)<\/span>/, '\1 ').gsub(/<.*?>/im, "")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require 'open-uri'
|
|
2
|
+
|
|
3
|
+
module Bible
|
|
4
|
+
class NABLookup
|
|
5
|
+
|
|
6
|
+
@@books = {}
|
|
7
|
+
|
|
8
|
+
def self.getURL(book, chapter = nil)
|
|
9
|
+
raise "chapter cannot be nil" if chapter.nil?
|
|
10
|
+
b = book.to_s.gsub(" ", "").downcase
|
|
11
|
+
case Bible::Books[book]
|
|
12
|
+
when Bible::Books["Song of Solomon".to_sym]
|
|
13
|
+
"http://www.usccb.org/nab/bible/songs/song#{chapter.to_s}.htm"
|
|
14
|
+
when Bible::Books[:Philemon], Bible::Books[:Obadiah], Bible::Books["2 John".to_sym], Bible::Books["3 John".to_sym], Bible::Books[:Jude]
|
|
15
|
+
"http://www.usccb.org/nab/bible/#{b}/#{b}.htm"
|
|
16
|
+
when Bible::Books[:Psalms]
|
|
17
|
+
"http://www.usccb.org/nab/bible/#{b}/psalm#{chapter.to_s}.htm"
|
|
18
|
+
else
|
|
19
|
+
"http://www.usccb.org/nab/bible/#{b}/#{b}#{chapter.to_s}.htm"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.get_ref(book, chapter = nil, verse = nil)
|
|
24
|
+
# TODO: Handle nil chapter
|
|
25
|
+
text = ((@@books[book] ||= {})[chapter] ||= open(getURL(book, chapter)).gets(nil))
|
|
26
|
+
scanner = StringScanner.new(text)
|
|
27
|
+
scanner.skip_until(/<DL>/)
|
|
28
|
+
scanner = StringScanner.new(scanner.check_until(/<\/DL>/))
|
|
29
|
+
unless verse.nil?
|
|
30
|
+
if scanner.skip_until(/<A.*?NAME="v#{verse}".*?>.*?#{verse}.*?<\/A>/)
|
|
31
|
+
scanner = StringScanner.new(scanner.check_until(/<\/DD>/))
|
|
32
|
+
else
|
|
33
|
+
return ""
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
scanner.rest.strip.gsub(/<SUP>.*?<\/SUP>/im, "").gsub(/<.*?>/im, "")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'open-uri'
|
|
2
|
+
|
|
3
|
+
module Bible
|
|
4
|
+
class RSVLookup
|
|
5
|
+
@@books = {}
|
|
6
|
+
|
|
7
|
+
def self.getURL(book, chapter)
|
|
8
|
+
raise "chapter cannot be nil" if chapter.nil?
|
|
9
|
+
|
|
10
|
+
case book
|
|
11
|
+
when %s(1 Kings)
|
|
12
|
+
b = "1Kgs"
|
|
13
|
+
when %s(2 Kings)
|
|
14
|
+
b = "2Kgs"
|
|
15
|
+
when :Job
|
|
16
|
+
b = "BJob"
|
|
17
|
+
when %s(Song of Solomon)
|
|
18
|
+
b = "Cant"
|
|
19
|
+
when :Philemon
|
|
20
|
+
b = "Phlm"
|
|
21
|
+
else
|
|
22
|
+
b = book.to_s.split(" ").join("")[0 ... 4]
|
|
23
|
+
end
|
|
24
|
+
"http://etext.lib.virginia.edu/etcbin/toccer-new2?id=Rsv#{b}.sgm&images=images/modeng&data=/texts/english/modeng/parsed&tag=public&part=#{chapter}&division=div1"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.get_ref(book, chapter , verse )
|
|
28
|
+
raise "Verse cannot be nil for RSV lookup." if verse.nil?
|
|
29
|
+
raise "Chapter cannot be nil for RSV lookup." if chapter.nil?
|
|
30
|
+
|
|
31
|
+
text = ((@@books[book] ||= {})[chapter] ||= open(getURL(book, chapter)).gets(nil))
|
|
32
|
+
scanner = StringScanner.new(text)
|
|
33
|
+
if ! verse.nil?
|
|
34
|
+
if ! scanner.skip_until(/<i>#{verse}:<\/i>/)
|
|
35
|
+
scanner = StringScanner.new("")
|
|
36
|
+
else
|
|
37
|
+
scanner = StringScanner.new(scanner.scan_until(/(<br>|<\/p>)/)[0 ... scanner[1].length * -1])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if scanner.empty?
|
|
42
|
+
""
|
|
43
|
+
else
|
|
44
|
+
scanner.rest
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/bible/parser.rb
ADDED
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'strscan'
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
# Contains classes to parse bible verses and
|
|
6
|
+
# an iterator to look them up given an appropriate bible lookup class.
|
|
7
|
+
module Bible
|
|
8
|
+
|
|
9
|
+
class ChapterInfo
|
|
10
|
+
attr_reader :book, :chapter
|
|
11
|
+
|
|
12
|
+
# Takes a BookInfo and count of verses in the chapter
|
|
13
|
+
def initialize(book, chapter, verse_count)
|
|
14
|
+
@book = book
|
|
15
|
+
@chapter = chapter
|
|
16
|
+
@verse_count = verse_count
|
|
17
|
+
@verse_range = Range.new(1, @verse_count)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns range of verses in this chapter
|
|
21
|
+
def verses
|
|
22
|
+
@verse_range
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class BookInfo
|
|
28
|
+
|
|
29
|
+
# Symbol naming book this information is about
|
|
30
|
+
attr_accessor :book, :abbreviations, :chapters
|
|
31
|
+
|
|
32
|
+
# Returns the BookInfo of the next book from this book
|
|
33
|
+
def succ
|
|
34
|
+
idx = BookInfo.all_books.index(@book)
|
|
35
|
+
BookInfo.all_books[idx + 1] unless idx.nil?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def <=>(value)
|
|
39
|
+
if value.respond_to?(:book)
|
|
40
|
+
oidx = BookInfo.all_books.index(value.book)
|
|
41
|
+
elsif value.is_a?(String)
|
|
42
|
+
oidx = BookInfo.all_books.index(value.to_sym)
|
|
43
|
+
elsif value.is_a?(Symbol)
|
|
44
|
+
oidx = BookInfo.all_books.index(value.to_s)
|
|
45
|
+
else
|
|
46
|
+
raise "Don't know how to compare BookInfo to #{value.inspect}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
return BookInfo.all_books.index(@book) <=> oidx
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Yields the BookInfo of each book, including this one, up to the next book.
|
|
53
|
+
def upto(b)
|
|
54
|
+
idx = BookInfo.all_books.index(@book)
|
|
55
|
+
endIdx = BookInfo.all_books.index(b)
|
|
56
|
+
while idx <= endIdx
|
|
57
|
+
yield BookInfo.all_books[idx]
|
|
58
|
+
idx += 1
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get the nth chapter (0 based)
|
|
63
|
+
def [](value)
|
|
64
|
+
@chapter_info[value]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
def initialize(book, num_chapters, verse_counts, abbreviations)
|
|
69
|
+
@book = book
|
|
70
|
+
# Create hash associating chapters to verses
|
|
71
|
+
@chapters = Range.new(1, num_chapters)
|
|
72
|
+
@chapter_info = ([*(@chapters)].zip(verse_counts).collect { |chapter_def| ChapterInfo.new(self, chapter_def[0], chapter_def[1]) })
|
|
73
|
+
@abbreviations = abbreviations.is_a?(Array) ? abbreviations : [abbreviations]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
public
|
|
77
|
+
|
|
78
|
+
def ==(value)
|
|
79
|
+
if value.respond_to?(:book)
|
|
80
|
+
return value.book == @book
|
|
81
|
+
elsif value.respond_to?(:to_s)
|
|
82
|
+
return @book.to_s == value.to_s || Books[value.to_s].book == @book
|
|
83
|
+
else
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Array of all books defined
|
|
89
|
+
def self.all_books
|
|
90
|
+
@@all_books
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get a specific book by passing a symbol representing its name
|
|
94
|
+
def self.[](book)
|
|
95
|
+
return all_books[all_books.index(book)]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
YAML::load_file(File.dirname(__FILE__) + "/bible.yml").each { |b|
|
|
99
|
+
n = b[0].to_sym
|
|
100
|
+
abb = b[1]
|
|
101
|
+
ch = b[2]
|
|
102
|
+
v = b[3]
|
|
103
|
+
|
|
104
|
+
# Add name of book to abbreviations, unless it's already there.
|
|
105
|
+
if abb.nil?
|
|
106
|
+
abb = [n.to_s]
|
|
107
|
+
elsif abb.is_a?(Array)
|
|
108
|
+
abb << n.to_s unless abb.include?(n.to_s)
|
|
109
|
+
else
|
|
110
|
+
((abb = [abb]) << n.to_s) unless abb == n.to_s
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
(@@all_books ||= []) << BookInfo.new(n, ch, v, abb)
|
|
114
|
+
}
|
|
115
|
+
end # end BookInfo
|
|
116
|
+
|
|
117
|
+
# Index all books by single 'canonical' symbol. Keys are symbols, values are BookInfo instances.
|
|
118
|
+
CanonicalBooks = Hash[*(BookInfo::all_books.collect do |b|
|
|
119
|
+
if b.book.is_a?(Array)
|
|
120
|
+
b.book.collect { |bn| [bn, b] }
|
|
121
|
+
else
|
|
122
|
+
[b.book, b]
|
|
123
|
+
end
|
|
124
|
+
end).flatten]
|
|
125
|
+
|
|
126
|
+
# Index all books by possible abbreviations. Keys are strings or symbols and values are BookInfo instances.
|
|
127
|
+
Books = Hash[*(BookInfo::all_books.collect { |b| b.abbreviations.collect { |a| [a.to_s.downcase, b] } }.flatten)]
|
|
128
|
+
|
|
129
|
+
class << Books
|
|
130
|
+
def[](value)
|
|
131
|
+
if value.is_a?(Symbol)
|
|
132
|
+
return CanonicalBooks[value]
|
|
133
|
+
else
|
|
134
|
+
super(value.to_s.downcase)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
class BibleRefParser
|
|
140
|
+
|
|
141
|
+
# Helper module which can is used by Verses and Chapters objects to compare themselves
|
|
142
|
+
# against Arrays and Ranges.
|
|
143
|
+
module RangeComparisons
|
|
144
|
+
# Compares the given ranges to the given value. value can hold an Array or a Range. An array
|
|
145
|
+
# can contain fixnums or ranges.
|
|
146
|
+
def compare_ranges(ranges, value, which)
|
|
147
|
+
return (value.empty? && ranges.empty?) if value.is_a?(Array) && (value.empty? || ranges.empty?)
|
|
148
|
+
return (value.nil? && ranges.empty?) if value.nil? || ranges.empty?
|
|
149
|
+
|
|
150
|
+
if value.is_a?(Range)
|
|
151
|
+
# Continuous ranges mean there are no nil elements in the array, since those represent
|
|
152
|
+
# discontinuous verses or chapters. Therefore, first step is to make sure ranges holds all non-nil items.
|
|
153
|
+
raise "Can't compare to range because #{which} are not continuous." unless ranges.nitems == ranges.length
|
|
154
|
+
return Range.new(ranges.first, ranges.last) == value
|
|
155
|
+
elsif value.is_a?(Array)
|
|
156
|
+
# compare all non-nil elements, since nils are just internal markers for our use
|
|
157
|
+
c = ranges.compact
|
|
158
|
+
offset = 0
|
|
159
|
+
value.each_with_index do |val, idx|
|
|
160
|
+
if val.is_a?(Range)
|
|
161
|
+
val.each do |range_value|
|
|
162
|
+
return false if range_value != c[idx + offset]
|
|
163
|
+
offset += 1
|
|
164
|
+
end
|
|
165
|
+
offset -= 1
|
|
166
|
+
else
|
|
167
|
+
return false if val != c[idx + offset]
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
return true
|
|
171
|
+
elsif value.respond_to?(:to_i)
|
|
172
|
+
raise "Can't compare single fixnum to multiple #{which}." unless ranges.length == 1
|
|
173
|
+
return ranges[0].to_i == value.to_i
|
|
174
|
+
else
|
|
175
|
+
raise "Don't know how to compare #{which} to value #{value.inspect}"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class Book
|
|
182
|
+
class SingleBook
|
|
183
|
+
attr_accessor :book_symbol, :chapter
|
|
184
|
+
|
|
185
|
+
# Argument should be a symbol from BookInfo class representing the book.
|
|
186
|
+
def initialize(book_symbol)
|
|
187
|
+
@book_symbol = book_symbol
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def book
|
|
191
|
+
self
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Fixes the book reference. If chapter is nil, assumed to refer to entire book. If
|
|
195
|
+
# chapters ends in -1, assumed to refere to chapters to end of book. If any chapters
|
|
196
|
+
# are contained in this book, they will all be fixed up.
|
|
197
|
+
def fixup
|
|
198
|
+
# No chapters at all means ALL chapters, so create the range for fixup.
|
|
199
|
+
if ! has_chapter?
|
|
200
|
+
book = Books[@book_symbol]
|
|
201
|
+
# By reference self.chapters, a refernce to Chapter 1 is created.
|
|
202
|
+
# More verbosely, we could assign self.chapter = Chapter 1 and then
|
|
203
|
+
# reference self.chapters, but there isn't much point.
|
|
204
|
+
self.chapters << Chapter.new(self, -1)
|
|
205
|
+
@chapters.fixup
|
|
206
|
+
elsif single_chapter?
|
|
207
|
+
@chapter.fixup
|
|
208
|
+
if @chapter.single_verse?
|
|
209
|
+
self.verse = @chapter.verse
|
|
210
|
+
end
|
|
211
|
+
else
|
|
212
|
+
@chapters.fixup
|
|
213
|
+
end
|
|
214
|
+
# Don't want reference to chapters method to create chapters after fixup
|
|
215
|
+
@fixedUp = true
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Assumes value is a symbol and compares it to the book represented
|
|
219
|
+
# here.
|
|
220
|
+
def ==(value)
|
|
221
|
+
return false if value.nil? || @book_symbol.nil?
|
|
222
|
+
if value.is_a?(Symbol)
|
|
223
|
+
return value == @book_symbol
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def has_chapter?
|
|
228
|
+
(respond_to?(:chapter) && ! @chapter.nil?) || (respond_to?(:chapters) && ! @chapters.nil?)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def single_chapter?
|
|
232
|
+
respond_to?(:chapter)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def single_verse?
|
|
236
|
+
single_chapter? && @chapter.single_verse?
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def method_missing(symbol, *args)
|
|
240
|
+
if ! @fixedUp
|
|
241
|
+
case symbol
|
|
242
|
+
# when chapters is referenced, remove the chapter method
|
|
243
|
+
# and add chapters method so this book has multiple chapters.
|
|
244
|
+
when :chapters
|
|
245
|
+
c = self.chapter
|
|
246
|
+
class << self
|
|
247
|
+
attr_reader :chapters
|
|
248
|
+
undef_method :chapter, :chapter=
|
|
249
|
+
end
|
|
250
|
+
remove_instance_variable :@chapter if defined?(@chapter)
|
|
251
|
+
|
|
252
|
+
@chapters = Chapters.new(self)
|
|
253
|
+
# If no initial chapter has been set, set it to 1
|
|
254
|
+
if c.nil?
|
|
255
|
+
@chapters << Chapter.new(self, 1)
|
|
256
|
+
else
|
|
257
|
+
@chapters << c
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
@chapters
|
|
261
|
+
when :verse, :verse=
|
|
262
|
+
class << self
|
|
263
|
+
define_method :verse do
|
|
264
|
+
@chapter.verse
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
define_method :verse= do |value|
|
|
268
|
+
@chapter.verse = value
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
self.__send__(symbol, *args)
|
|
273
|
+
else
|
|
274
|
+
super(symbol, *args)
|
|
275
|
+
end
|
|
276
|
+
else
|
|
277
|
+
super(symbol, *args)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def inspect
|
|
282
|
+
if single_chapter?
|
|
283
|
+
"#{@book_symbol} " + @chapter.inspect
|
|
284
|
+
else
|
|
285
|
+
"#{@book_symbol} " + @chapters.inspect
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
class MultipleBooks
|
|
292
|
+
def initialize(single)
|
|
293
|
+
@books = [single]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def fixup
|
|
297
|
+
if ! @fixedUp
|
|
298
|
+
@books.compact.each { |b| b.fixup }
|
|
299
|
+
@fixedUp = true
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def <<(value)
|
|
304
|
+
unless value.nil? || @books.last.nil?
|
|
305
|
+
Books[@books.last.book_symbol].succ.upto(value) { |book_info|
|
|
306
|
+
@books << SingleBook.new(book_info.book)
|
|
307
|
+
}
|
|
308
|
+
else
|
|
309
|
+
if value.nil?
|
|
310
|
+
@books << value
|
|
311
|
+
else
|
|
312
|
+
@books << SingleBook.new(value)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def books
|
|
318
|
+
@books.compact
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def ==(value)
|
|
322
|
+
raise "Can't compare multipe books to single book" unless value.is_a?(Array)
|
|
323
|
+
@books.compact.each_with_index { |b, idx|
|
|
324
|
+
raise "Don't know how to compare to book #{value[idx]}" unless value[idx].is_a?(String) || value[idx].is_a?(Symbol)
|
|
325
|
+
}
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def method_missing(sym, *args)
|
|
329
|
+
# This forwarding is used only until fixup because the parse
|
|
330
|
+
# algorithm depends on it to query the "current reference" in a multiple
|
|
331
|
+
# book situation
|
|
332
|
+
if ! @fixedUp
|
|
333
|
+
@books.last.__send__(sym, *args)
|
|
334
|
+
else
|
|
335
|
+
super(sym, *args)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def inspect
|
|
340
|
+
@books.compact.inject("") { |val, b|
|
|
341
|
+
val << " " unless val.empty?
|
|
342
|
+
val << b.inspect
|
|
343
|
+
val
|
|
344
|
+
}
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def has_book?
|
|
350
|
+
! @proxy.nil?
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def single_book?
|
|
354
|
+
self.has_book? && @proxy.is_a?(SingleBook)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def <<(value)
|
|
358
|
+
if @proxy.nil?
|
|
359
|
+
@proxy = SingleBook.new(value)
|
|
360
|
+
else
|
|
361
|
+
@proxy = MultipleBooks.new(@proxy) if @proxy.is_a?(SingleBook)
|
|
362
|
+
@proxy << value
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def method_missing(symbol, *args)
|
|
367
|
+
if @proxy.nil?
|
|
368
|
+
super(symbol, *args)
|
|
369
|
+
else
|
|
370
|
+
@proxy.__send__(symbol, *args)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
class Chapter
|
|
377
|
+
include Comparable
|
|
378
|
+
include Enumerable
|
|
379
|
+
|
|
380
|
+
attr_reader :book, :chapter_number
|
|
381
|
+
attr_accessor :verse
|
|
382
|
+
|
|
383
|
+
def initialize(book, chapter)
|
|
384
|
+
raise "Chapter must be an integer" unless chapter.is_a?(Fixnum)
|
|
385
|
+
|
|
386
|
+
@book = book
|
|
387
|
+
@chapter_number = chapter
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def fixup
|
|
391
|
+
if ! has_verse?
|
|
392
|
+
self.verses << Verse.new(self, -1)
|
|
393
|
+
self.verses.fixup
|
|
394
|
+
elsif ! single_verse?
|
|
395
|
+
@verses.fixup
|
|
396
|
+
end
|
|
397
|
+
# Don't want reference to verses method to create chapters after fixup
|
|
398
|
+
@fixedUp = true
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Assumes value is a Fixnum and determines if it is equal
|
|
402
|
+
# to this chapter.
|
|
403
|
+
def ==(value)
|
|
404
|
+
return false if value.nil? || @chapter_number.nil?
|
|
405
|
+
raise "Don't know how to compare chapter to #{value.inspect}" unless value.is_a?(Fixnum)
|
|
406
|
+
return value == @chapter_number
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def to_i
|
|
410
|
+
return @chapter_number.to_i
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def to_s
|
|
414
|
+
return @chapter_number.to_s
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def <=>(value)
|
|
418
|
+
return -1 if value.nil?
|
|
419
|
+
raise "Don't know how to compare Chapter and #{value.inspect} becuase it does not implement to_i." unless value.respond_to?(:to_i)
|
|
420
|
+
return @chapter_number.to_i <=> value.to_i
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def succ
|
|
424
|
+
return Chapter.new(@book, @chapter_number + 1)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def upto(i)
|
|
428
|
+
raise "Cannot enumerate to value #{i.inspect} because it does not respond to to_i." unless i.respond_to?(:to_i)
|
|
429
|
+
yield self
|
|
430
|
+
(@chapter_number + 1).upto(i.to_i) { |x| yield(Chapter.new(@book, x)) }
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def has_verse?
|
|
434
|
+
(respond_to?(:verse) && ! @verse.nil?) || (respond_to?(:verses) && ! @verses.nil?)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def single_verse?
|
|
438
|
+
respond_to?(:verse)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def method_missing(symbol, *args)
|
|
442
|
+
unless @fixedUp
|
|
443
|
+
case symbol
|
|
444
|
+
# when chapters is referenced, remove the chapter method
|
|
445
|
+
# and add chapters method so this book has multiple chapters.
|
|
446
|
+
when :verses
|
|
447
|
+
c = self.verse
|
|
448
|
+
class << self
|
|
449
|
+
attr_reader :verses
|
|
450
|
+
undef_method :verse, :verse=
|
|
451
|
+
end
|
|
452
|
+
remove_instance_variable :@verse if defined?(@verse)
|
|
453
|
+
|
|
454
|
+
@verses = Verses.new(self)
|
|
455
|
+
# If no initial verse, set it to 1
|
|
456
|
+
if c.nil?
|
|
457
|
+
@verses << Verse.new(self, 1)
|
|
458
|
+
else
|
|
459
|
+
@verses << c
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
@verses
|
|
463
|
+
else
|
|
464
|
+
super(symbol, *args)
|
|
465
|
+
end
|
|
466
|
+
else
|
|
467
|
+
super(symbol, *args)
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def inspect
|
|
472
|
+
if single_verse?
|
|
473
|
+
"#{@chapter_number}:" + @verse.inspect
|
|
474
|
+
else
|
|
475
|
+
"#{@chapter_number}:" + @verses.inspect
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
class Verse
|
|
481
|
+
include Comparable
|
|
482
|
+
include Enumerable
|
|
483
|
+
|
|
484
|
+
attr_reader :chapter, :verse_number
|
|
485
|
+
attr_accessor :verse
|
|
486
|
+
|
|
487
|
+
def initialize(chapter, verse)
|
|
488
|
+
raise "Verse must be an integer" unless verse.is_a?(Fixnum)
|
|
489
|
+
|
|
490
|
+
@chapter = chapter
|
|
491
|
+
@verse_number = verse
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Assumes value is a Fixnum and determines if it is equal
|
|
495
|
+
# to this verse.
|
|
496
|
+
def ==(value)
|
|
497
|
+
return false if value.nil? || @verse_number.nil?
|
|
498
|
+
raise "Don't know how to compare verse to #{value.inspect}" unless value.is_a?(Fixnum)
|
|
499
|
+
return value == @verse_number
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def to_i
|
|
503
|
+
return @verse_number.to_i
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def to_s
|
|
507
|
+
return @verse_number.to_s
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def <=>(value)
|
|
511
|
+
return -1 if value.nil?
|
|
512
|
+
raise "Don't know how to compare Verse and #{value.inspect} becuase it does not implement to_i." unless value.respond_to?(:to_i)
|
|
513
|
+
return @verse_number.to_i <=> value.to_i
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def succ
|
|
517
|
+
return Verse.new(@chapter, @verse_number + 1)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def upto(i)
|
|
521
|
+
raise "Cannot enumerate to value #{i.inspect} because it does not respond to to_i." unless i.respond_to?(:to_i)
|
|
522
|
+
yield(self)
|
|
523
|
+
(@verse_number + 1).upto(i.to_i) { |x| yield(Verse.new(@chapter, x)) }
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def inspect
|
|
527
|
+
self.to_s
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Represents range or discontinuous series of chaptesr
|
|
532
|
+
class Chapters
|
|
533
|
+
include RangeComparisons
|
|
534
|
+
|
|
535
|
+
def initialize(book)
|
|
536
|
+
@chapters = []
|
|
537
|
+
@book = book
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def fixup
|
|
541
|
+
if @chapters.last == -1
|
|
542
|
+
book = Books[@book.book_symbol]
|
|
543
|
+
@chapters.pop
|
|
544
|
+
@chapters.last.succ.upto(book.chapters.last) do |c|
|
|
545
|
+
@chapters << c
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
@chapters.compact.each { |c| c.fixup }
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def <<(value)
|
|
553
|
+
unless value.nil? || @chapters.empty? || @chapters.last.nil? || value == -1
|
|
554
|
+
raise "Cannot add a chapter reference in reverse order" if @chapters.last > value
|
|
555
|
+
unless @chapters.last.chapter_number == value
|
|
556
|
+
@chapters.last.succ.upto(value) { |c|
|
|
557
|
+
@chapters << c
|
|
558
|
+
}
|
|
559
|
+
end
|
|
560
|
+
else
|
|
561
|
+
@chapters << value
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def length
|
|
566
|
+
@chapters.length
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def last
|
|
570
|
+
@chapters.last
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def [](index)
|
|
574
|
+
return @chapters.compact[index]
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def ==(value)
|
|
578
|
+
compare_ranges(@chapters, value, "chapters")
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def inspect
|
|
582
|
+
@chapters.inject("") { |val, c|
|
|
583
|
+
val << "," unless val.empty?
|
|
584
|
+
val << c.inspect unless c.nil?
|
|
585
|
+
val
|
|
586
|
+
}
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Returns Chapter objects for each chapter, or -1 if "end of chapters" is indicated.
|
|
590
|
+
# If chapters is empty, will be a no-op
|
|
591
|
+
def each
|
|
592
|
+
return if @chapters.empty?
|
|
593
|
+
@chapters.each { |chapter|
|
|
594
|
+
yield chapter unless chapter.nil?
|
|
595
|
+
}
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# Represents range or discontinuous series of chaptesr
|
|
600
|
+
class Verses
|
|
601
|
+
include RangeComparisons
|
|
602
|
+
|
|
603
|
+
def initialize(chapter)
|
|
604
|
+
@verses = []
|
|
605
|
+
@chapter = chapter
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def fixup
|
|
609
|
+
if @verses.last == -1
|
|
610
|
+
@verses.pop
|
|
611
|
+
chapter = Books[@chapter.book.book_symbol][@chapter.chapter_number - 1]
|
|
612
|
+
@verses.last.succ.upto(chapter.verses.last) do |v|
|
|
613
|
+
@verses << v
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def <<(value)
|
|
619
|
+
unless value.nil? || @verses.empty? || @verses.last.nil? || value == -1
|
|
620
|
+
raise "Cannot add a verse reference in reverse order" if @verses.last > value
|
|
621
|
+
unless @verses.last.verse_number == value
|
|
622
|
+
@verses.last.succ.upto(value) { |v|
|
|
623
|
+
@verses << v
|
|
624
|
+
}
|
|
625
|
+
end
|
|
626
|
+
else
|
|
627
|
+
@verses << value
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def length
|
|
632
|
+
@verses.length
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def last
|
|
636
|
+
@verses.last
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def [](index)
|
|
640
|
+
return @verses.compact[index]
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def ==(value)
|
|
644
|
+
compare_ranges(@verses, value, "verses")
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# All verses defined on the chapter. If none are defined, this is a no-op.
|
|
648
|
+
# Will yield either Verse objects or -1 (indicating go to end of verses).
|
|
649
|
+
def each
|
|
650
|
+
return if @verses.empty?
|
|
651
|
+
@verses.each { |verse|
|
|
652
|
+
yield verse unless verse.nil?
|
|
653
|
+
}
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def inspect
|
|
657
|
+
last_verse = nil
|
|
658
|
+
s = @verses.inject("") { |val, v|
|
|
659
|
+
if val.empty? || last_verse.nil?
|
|
660
|
+
val << "," unless val.empty?
|
|
661
|
+
val << v.inspect
|
|
662
|
+
elsif v.nil?
|
|
663
|
+
val << "-#{last_verse.inspect}"
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
last_verse = v
|
|
667
|
+
val
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
s << "-#{last_verse.inspect}" unless last_verse.nil?
|
|
671
|
+
s
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# determines if a book reference is coming up in the string. Does not
|
|
676
|
+
# determine if its a valid book - jsut the form of one.
|
|
677
|
+
def self.book_ahead?(str)
|
|
678
|
+
return ! get_book(str).nil?
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# Returns the upcoming book, if any. If modify is true, the string passed in has the book referenced removed
|
|
682
|
+
def self.get_book(scanner, modify = false)
|
|
683
|
+
# Look for numbered book references (a digit, followed by whitespace, following by some number of word characters.)
|
|
684
|
+
if t = scanner.check(/\d\s+[A-Za-z]+(\.|)/)
|
|
685
|
+
if modify
|
|
686
|
+
return scanner.scan(/\d\s+[A-Za-z]+(\.|)/).gsub(/\./, "")
|
|
687
|
+
else
|
|
688
|
+
return t
|
|
689
|
+
end
|
|
690
|
+
# Look for one of the 'normal' books
|
|
691
|
+
elsif t = scanner.check(/[A-Za-z]+(\.|)/)
|
|
692
|
+
if modify
|
|
693
|
+
return scanner.scan(/[A-Za-z]+(\.|)/).gsub(/\./, "")
|
|
694
|
+
else
|
|
695
|
+
return t
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
return nil
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def self.parse(reference)
|
|
703
|
+
return nil if reference.nil? || (reference = reference.strip) == ""
|
|
704
|
+
|
|
705
|
+
state = :book
|
|
706
|
+
# will use curr_ref to build up each reference
|
|
707
|
+
curr_ref = Book.new
|
|
708
|
+
|
|
709
|
+
ref = StringScanner.new(reference)
|
|
710
|
+
while ! ref.eos?
|
|
711
|
+
ref.skip(/\s*/)
|
|
712
|
+
if ! ref.eos?
|
|
713
|
+
token = ""
|
|
714
|
+
case state
|
|
715
|
+
when :book, :end_book
|
|
716
|
+
# Special case - one of the numbered books.
|
|
717
|
+
token = get_book(ref, true)
|
|
718
|
+
|
|
719
|
+
raise "Book #{token} not recognized in #{ref.rest}" if Books[token].nil?
|
|
720
|
+
|
|
721
|
+
if state == :end_book
|
|
722
|
+
curr_ref << Books[token].book
|
|
723
|
+
state = :end_chapter
|
|
724
|
+
elsif curr_ref.has_book?
|
|
725
|
+
# add discontinuity if there is a previous book defined.
|
|
726
|
+
curr_ref << nil if curr_ref.has_book?
|
|
727
|
+
curr_ref << Books[token].book
|
|
728
|
+
state = :start_chapter
|
|
729
|
+
else
|
|
730
|
+
# look for tokens indicating another book is ahead, rather than a chapter
|
|
731
|
+
curr_ref << Books[token].book
|
|
732
|
+
if ref.skip(/\s+(-|,|;|$)/)
|
|
733
|
+
state = :end_book
|
|
734
|
+
else
|
|
735
|
+
state = :start_chapter
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
when :start_chapter, :end_chapter
|
|
739
|
+
|
|
740
|
+
# Get all digits, assume they are a chapter
|
|
741
|
+
token << ref.scan(/\d+/)
|
|
742
|
+
chapter = token.to_i rescue (raise "Chapter not recognized as integer #{token}")
|
|
743
|
+
|
|
744
|
+
case state
|
|
745
|
+
when :start_chapter
|
|
746
|
+
if ! curr_ref.has_chapter?
|
|
747
|
+
curr_ref.chapter = Chapter.new(curr_ref.book, chapter)
|
|
748
|
+
else
|
|
749
|
+
curr_ref.chapters << Chapter.new(curr_ref.book, chapter)
|
|
750
|
+
end
|
|
751
|
+
when :end_chapter
|
|
752
|
+
# Handles the case where this is a book-spanning reference.
|
|
753
|
+
if ! curr_ref.has_chapter?
|
|
754
|
+
if chapter > 1
|
|
755
|
+
curr_ref.chapters << Chapter.new(curr_ref.book, chapter)
|
|
756
|
+
else
|
|
757
|
+
curr_ref.chapter = Chapter.new(curr_ref.book, chapter)
|
|
758
|
+
end
|
|
759
|
+
elsif (! curr_ref.single_chapter?) || curr_ref.chapter != chapter
|
|
760
|
+
curr_ref.chapters << Chapter.new(curr_ref.book, chapter)
|
|
761
|
+
end
|
|
762
|
+
end
|
|
763
|
+
token = ""
|
|
764
|
+
|
|
765
|
+
# peek ahead to determine next place to go
|
|
766
|
+
ref.skip(/\s*/)
|
|
767
|
+
case state
|
|
768
|
+
when :start_chapter
|
|
769
|
+
case ref.peek(1)
|
|
770
|
+
when ":", "."
|
|
771
|
+
ref.getch
|
|
772
|
+
state = :start_verse
|
|
773
|
+
when "-"
|
|
774
|
+
ref.getch
|
|
775
|
+
ref.skip(/\s*/)
|
|
776
|
+
if book_ahead?(ref)
|
|
777
|
+
# indicate we need to go to end of chapters for this book
|
|
778
|
+
curr_ref.book.chapters << Chapter.new(curr_ref.book, -1)
|
|
779
|
+
state = :end_book
|
|
780
|
+
else
|
|
781
|
+
state = :end_chapter
|
|
782
|
+
end
|
|
783
|
+
when ",", ";"
|
|
784
|
+
ref.getch
|
|
785
|
+
ref.skip(/\s*/)
|
|
786
|
+
curr_ref.chapters << nil
|
|
787
|
+
# If a word character comes up next, we are looking at a book reference
|
|
788
|
+
if book_ahead?(ref)
|
|
789
|
+
state = :book
|
|
790
|
+
else
|
|
791
|
+
state = :start_chapter
|
|
792
|
+
end
|
|
793
|
+
when ""
|
|
794
|
+
ref.getch
|
|
795
|
+
break
|
|
796
|
+
else
|
|
797
|
+
raise "Unrecognized token after verse: #{ref.rest}"
|
|
798
|
+
end
|
|
799
|
+
when :end_chapter
|
|
800
|
+
case ref.peek(1)
|
|
801
|
+
when ",", ";"
|
|
802
|
+
ref.getch
|
|
803
|
+
ref.skip(/\s*/)
|
|
804
|
+
# add nil to indicate discontinuity
|
|
805
|
+
curr_ref.chapters << nil
|
|
806
|
+
if book_ahead?(ref)
|
|
807
|
+
state = :book
|
|
808
|
+
else
|
|
809
|
+
state = :start_chapter
|
|
810
|
+
end
|
|
811
|
+
when ":", "."
|
|
812
|
+
ref.getch
|
|
813
|
+
state = :end_verse
|
|
814
|
+
when ""
|
|
815
|
+
ref.getch
|
|
816
|
+
break
|
|
817
|
+
else
|
|
818
|
+
raise "Unrecognized token after end chapter: #{ref.ref}"
|
|
819
|
+
end
|
|
820
|
+
end
|
|
821
|
+
when :start_verse , :end_verse
|
|
822
|
+
|
|
823
|
+
token << ref.scan(/\d+/) if ! ref.eos?
|
|
824
|
+
verse = token.to_i rescue (raise "Verse not recognized as integer #{token}")
|
|
825
|
+
|
|
826
|
+
case state
|
|
827
|
+
when :start_verse
|
|
828
|
+
cp = curr_ref.single_chapter? ? curr_ref.chapter : curr_ref.chapters.last
|
|
829
|
+
if ! cp.has_verse?
|
|
830
|
+
cp.verse = Verse.new(cp, verse)
|
|
831
|
+
elsif (! cp.single_verse?) || cp.verse != verse
|
|
832
|
+
cp.verses << nil
|
|
833
|
+
cp.verses << Verse.new(cp, verse)
|
|
834
|
+
end
|
|
835
|
+
when :end_verse
|
|
836
|
+
cp = curr_ref.single_chapter? ? curr_ref.chapter : curr_ref.chapters.last
|
|
837
|
+
if ! cp.has_verse?
|
|
838
|
+
if verse > 1
|
|
839
|
+
cp.verses << Verse.new(cp, verse)
|
|
840
|
+
else
|
|
841
|
+
cp.verse = Verse.new(cp, verse)
|
|
842
|
+
end
|
|
843
|
+
elsif (! cp.single_verse?) || cp.verse != verse
|
|
844
|
+
cp.verses << Verse.new(cp, verse)
|
|
845
|
+
end
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
token = ""
|
|
849
|
+
|
|
850
|
+
# Skip whitespace and single character references (for references such as "Acts 1:1a")
|
|
851
|
+
ref.skip(/[a-zA-Z]?\s*/)
|
|
852
|
+
case ref.peek(1)
|
|
853
|
+
when "-"
|
|
854
|
+
raise "Unrecognized token after #{ref}" if state == :end_verse
|
|
855
|
+
ref.getch
|
|
856
|
+
ref.skip(/\s*/)
|
|
857
|
+
# Determine if we are actually looking at a chapter or book spanning reference.
|
|
858
|
+
# We start by assuming we'll see a verse refernce next, and first look for a book.
|
|
859
|
+
state = :end_verse
|
|
860
|
+
if book_ahead?(ref)
|
|
861
|
+
curr_ref.chapters.last.verses << Verse.new(curr_ref.chapters.last, -1)
|
|
862
|
+
curr_ref.chapters << Chapter.new(curr_ref.book, -1)
|
|
863
|
+
state = :end_book
|
|
864
|
+
else
|
|
865
|
+
# No book was found, so now look for a chapter spannign reference. We do this
|
|
866
|
+
# by seeing if a colon follows the expected numeric reference.
|
|
867
|
+
if ref.check(/\d+(:|\.)/)
|
|
868
|
+
# This is actually a chapter reference, not a verse reference. Indicate our current verse should
|
|
869
|
+
# go to the "end", and then go to chapter parsing
|
|
870
|
+
curr_ref.chapters.last.verses << Verse.new(curr_ref.chapters.last, -1)
|
|
871
|
+
state = :end_chapter
|
|
872
|
+
end
|
|
873
|
+
end
|
|
874
|
+
when ",", ";"
|
|
875
|
+
ref.getch
|
|
876
|
+
ref.skip(/\s*/)
|
|
877
|
+
|
|
878
|
+
# Need to cheat and determine if verse, chapter:verse, or book lie ahead
|
|
879
|
+
# assume we are going to see another verse
|
|
880
|
+
state = :start_verse
|
|
881
|
+
|
|
882
|
+
if book_ahead?(ref)
|
|
883
|
+
state = :book
|
|
884
|
+
else
|
|
885
|
+
if ref.check(/\d+(:|\.)/)
|
|
886
|
+
curr_ref.chapters << nil
|
|
887
|
+
state = :start_chapter
|
|
888
|
+
elsif ! ref.check(/\d+(,|-|;|$)/)
|
|
889
|
+
raise "Unrecognized token after end of verse: '#{ref}'"
|
|
890
|
+
end
|
|
891
|
+
end
|
|
892
|
+
when ""
|
|
893
|
+
ref.getch
|
|
894
|
+
else
|
|
895
|
+
raise "Unrecognized token after verse: #{ref.rest}"
|
|
896
|
+
end
|
|
897
|
+
end
|
|
898
|
+
end
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
curr_ref.fixup
|
|
902
|
+
return curr_ref
|
|
903
|
+
end
|
|
904
|
+
end
|
|
905
|
+
end
|