Class: WCC::Scripture::Esv

Inherits:
Object
  • Object
show all
Defined in:
lib/wcc/scripture/esv.rb

Overview

rubocop:disable Metrics/ClassLength

Defined Under Namespace

Classes: Reference

Constant Summary collapse

DATA_LIB =
File.join(__dir__, '../../../data/esv')
FULL_SANITIZER =
Rails::Html::FullSanitizer.new
XML_LOOKUP =
{
  'Psalm' => 'Psalms'
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(reference, footnotes: true, data_lib: DATA_LIB) ⇒ Esv

Returns a new instance of Esv.



57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/wcc/scripture/esv.rb', line 57

def initialize(
  reference,
  footnotes: true,
  data_lib: DATA_LIB
)
  raise WCC::Scripture::ScriptureNotFoundError unless @parsed_ref = Reference.parse(reference)

  @data_lib = data_lib
  @body_nodes = []
  @footnote_nodes = []
  @display_options = { footnotes: footnotes }
end

Instance Attribute Details

#parsed_refObject (readonly)

Returns the value of attribute parsed_ref.



9
10
11
# File 'lib/wcc/scripture/esv.rb', line 9

def parsed_ref
  @parsed_ref
end

Class Method Details

.lookup_xml(book_name, data_lib = DATA_LIB) ⇒ Object



311
312
313
314
315
316
# File 'lib/wcc/scripture/esv.rb', line 311

def self.lookup_xml(book_name, data_lib = DATA_LIB)
  Nokogiri::XML(
    File.open(File.join(data_lib, "#{XML_LOOKUP.fetch(book_name, book_name)}.xml")),
    &:huge
  )
end

Instance Method Details

#book_xmlObject



318
319
320
# File 'lib/wcc/scripture/esv.rb', line 318

def book_xml
  @book_xml ||= self.class.lookup_xml(@parsed_ref.book_name, @data_lib)
end

#convert_node(node, name, css = nil) ⇒ Object



131
132
133
134
135
136
# File 'lib/wcc/scripture/esv.rb', line 131

def convert_node(node, name, css = nil)
  Nokogiri::XML::Node.new(name, book_xml).tap do |heading|
    heading['class'] = css || node['class'] if css || node['class']
    heading.children = node.children
  end
end

#footnote(marker, note_node, id) ⇒ Object



279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/wcc/scripture/esv.rb', line 279

def footnote(marker, note_node, id)
  preprocess(note_node)

  verse_ref = Pericope::Verse.parse(marker_numeric_value(marker))

  postprocess(<<~HTML)
    <span class="footnote">
      <a href="#fb#{id}-1" id="f#{id}-1">[#{id}]</a>
    </span>
    <span class="footnote-ref">#{verse_ref.to_s(with_chapter: true)}</span>
    #{note_node}
  HTML
end

#footnote_anchor(note_node, id) ⇒ Object



271
272
273
274
275
276
277
# File 'lib/wcc/scripture/esv.rb', line 271

def footnote_anchor(note_node, id)
  <<~HTML
    <sup class="footnote">
      <a class="fn" href="#f#{id}-1" id="fb#{id}-1" title="#{FULL_SANITIZER.sanitize(note_node.inner_html)}">#{id}</a>
    </sup>
  HTML
end


146
147
148
149
150
151
152
153
154
# File 'lib/wcc/scripture/esv.rb', line 146

def link_traverse(bounds, node = nil)
  return unless node
  return if out_of_bounds?(bounds, node)

  @body_nodes.push(node) if node.name =~ /marker|heading|paragraph|verse/

  link_traverse(bounds, node.first_element_child) if node.name == 'chapter'
  link_traverse(bounds, node.next)
end

#marker_numeric_value(node) ⇒ Object



303
304
305
# File 'lib/wcc/scripture/esv.rb', line 303

def marker_numeric_value(node)
  node.attributes['mid'].value.sub(/^v/, '').to_i
end

#note_xmlObject



322
323
324
325
# File 'lib/wcc/scripture/esv.rb', line 322

def note_xml
  @note_xml ||=
    Nokogiri::XML(File.open(File.join(@data_lib, '_notes.text.xml')), &:huge)
end

#out_of_bounds?(bounds, node) ⇒ Boolean

Returns:

  • (Boolean)


293
294
295
296
297
# File 'lib/wcc/scripture/esv.rb', line 293

def out_of_bounds?(bounds, node)
  return unless verse_marker?(node)

  bounds.to_i < marker_numeric_value(node)
end

#parse_footnotes(marker, node) ⇒ Object



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/wcc/scripture/esv.rb', line 246

def parse_footnotes(marker, node)
  notes = node.search('note')

  return if notes.empty?
  return notes.remove unless @display_options[:footnotes]

  notes.each do |note|
    footnote_node = note_xml.at_css("note[id=\"#{note.attributes['nid'].value}\"]")

    if footnote_node
      footnote_id = @footnote_nodes.size + 1
      @footnote_nodes.push(
        footnote(marker, footnote_node, footnote_id)
      )
      note.replace(
        footnote_anchor(footnote_node, footnote_id)
      )
    else
      note.remove
    end
  end
end

#parse_nodesObject



138
139
140
141
142
143
144
# File 'lib/wcc/scripture/esv.rb', line 138

def parse_nodes
  @parsed_ref.ranges.each do |range|
    first, *_, last = range.to_a

    link_traverse(last || first, book_xml.at_css("marker.begin-verse[mid*=\"#{first.to_id}\"]"))
  end
end

#postprocess(markup) ⇒ Object



215
216
217
218
219
220
221
# File 'lib/wcc/scripture/esv.rb', line 215

def postprocess(markup)
  translate_entities(
    translate_pseudo_markup(
      markup
    )
  )
end

#preprocess(node) ⇒ Object



189
190
191
192
193
194
195
196
197
# File 'lib/wcc/scripture/esv.rb', line 189

def preprocess(node)
  node.tap do |n|
    n.search('crossref, crossrefs, begin-block-indent, end-block-indent, q.continue-double').remove
    translate_quotes(n)
    n.search('heading, subheading').each { |h| h.replace(convert_node(h, node.name == 'heading' ? 'h3' : 'h4')) }
    n.search('span.divine-name').each { |q| q.replace('LORD') }
    n.search('woc, selah').each { |span| span.replace(convert_node(span, 'span', span.name)) }
  end
end

#process_heading(marker, node) ⇒ Object



156
157
158
159
160
161
162
163
164
# File 'lib/wcc/scripture/esv.rb', line 156

def process_heading(marker, node)
  heading = convert_node(node, node.name == 'heading' ? 'h3' : 'h4')

  preprocess(heading)

  parse_footnotes(marker, heading)

  postprocess(heading.to_xml)
end

#process_verse(marker_node, verse_node) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/wcc/scripture/esv.rb', line 166

def process_verse(marker_node, verse_node)
  preprocess(verse_node)
  parse_footnotes(marker_node, verse_node)

  [
    %(<b class="verse-num">#{verse_node.attributes['num'].value}&nbsp;</b>),
    # We have to replace these strings in this fashion instead of using Nokogiri because the source markup
    # is not well-formed. The nodes do not encapsulate the text and children they are intending to designate as children,
    # but instead self-closing which presents a parsing nightmare. In an ideal world, we would be able to replace the
    # pseudo-html markup with real html and be done, but alas. So, for now, this post-processing step is acheived by
    # string replacement.
    #
    # Nokogiri HTML Serialization Options
    # 2 - exclude XML Declaration tag
    # 32 - output XML - prevents Nokogiri from closing our tags for us
    #
    # No option passed would assume HTML options, and disrupt our markup scheme, producing something like:
    # '<begin-line/>' => '<begin-line></begin-line>'
    postprocess(verse_node.inner_html(save_with: 2 | 32)),
    ' '
  ].join
end

#render_bodyObject

rubocop:disable Metrics/MethodLength rubocop:disable Metrics/CyclomaticComplexity rubocop:disable Metrics/AbcSize



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/wcc/scripture/esv.rb', line 81

def render_body
  marker = nil
  body = []
  para_open = false

  @body_nodes.each do |node|
    marker = node if verse_marker?(node)
    if node.name == 'heading'
      body.push process_heading(marker, node)
    elsif node.name == 'subheading'
      body.push process_heading(marker, node)
    elsif node.name == 'begin-paragraph'
      if node.attributes['class']&.value == 'line-group'
        body.push('<p class="block-indent">')
      else
        body.push '<p>'
      end
      para_open = true
    elsif node.name == 'end-paragraph'
      body.push '</p>'
      para_open = false
    elsif node.name == 'verse'
      unless para_open
        body.push '<p>'
        para_open = true
      end
      body.push process_verse(marker, node)
    end
  end

  # In case verse range didn't include end-paragraph
  body.push('</p>') if para_open

  body.compact.join
end

#render_footnotesObject

rubocop:enable Metrics/MethodLength rubocop:enable Metrics/CyclomaticComplexity rubocop:enable Metrics/AbcSize



120
121
122
123
124
125
126
127
128
129
# File 'lib/wcc/scripture/esv.rb', line 120

def render_footnotes
  return '' if @footnote_nodes.empty? || !@display_options[:footnotes]

  <<~HTML
    <div class="footnotes extra_text">
      <h3>Footnotes</h3>
      #{@footnote_nodes.join('<br/>')}
    </div>
  HTML
end

#render_htmlObject



70
71
72
73
74
75
76
# File 'lib/wcc/scripture/esv.rb', line 70

def render_html
  parse_nodes
  [
    render_body,
    render_footnotes
  ].join
end

#translate_entities(markup) ⇒ Object



233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/wcc/scripture/esv.rb', line 233

def translate_entities(markup)
  markup
    .gsub('&ldblquot;', '&#8220;')
    .gsub('&rdblquot;', '&#8221;')
    .gsub('&lquot;', '&#8216;')
    .gsub('&rquot;', '&#8217;')
    .gsub('&apos;', '&#8217;')
    .gsub('&emdash;', '&#8212;')
    .gsub('&endash;', '&#8211;')
    .gsub('&ellipsis;', '&nbsp;.&nbsp;.&nbsp;.')
    .gsub('&ellipsis4;', '.&nbsp;.&nbsp;.&nbsp;.')
end

#translate_pseudo_markup(markup) ⇒ Object



223
224
225
226
227
228
229
230
231
# File 'lib/wcc/scripture/esv.rb', line 223

def translate_pseudo_markup(markup)
  markup
    .gsub(%r{<begin-paragraph class="line-group"/>}, '<p class="block-indent">')
    .gsub(%r{<begin-paragraph(?:\s+class=".+")?/>}, '<p>')
    .gsub(%r{<end-paragraph/>}, '</p>')
    .gsub(%r{<end-line(?:\s+class=".+")?/>}, '</span><br/>')
    .gsub(%r{<begin-line\s?/>}, '<span class="line">')
    .gsub(%r{<begin-line\s+class="(.+)"/>}, '<span class="line \1">&nbsp;&nbsp;&nbsp;&nbsp;')
end

#translate_quotes(node) ⇒ Object

(

begin-double|continue-double|end-double
begin-inner-double|continue-inner-double|end-inner-double
begin-single|continue-single|end-single
begin-inner-single|end-inner-single

)



205
206
207
208
209
210
211
212
213
# File 'lib/wcc/scripture/esv.rb', line 205

def translate_quotes(node)
  node.tap do |n|
    n.search('q.begin-double, q.continue-double, q.begin-inner-double, q.continue-inner-double')
      .each { |q| q.replace('&#x201C;') }
    n.search('q.end-double, q.end-inner-double').each { |q| q.replace('&#x201D;') }
    n.search('q.begin-single, q.continue-single, q.begin-inner-single').each { |q| q.replace('&#x2018;') }
    n.search('q.end-single, q.end-inner-single').each { |q| q.replace('&#x2019;') }
  end
end

#verse_marker?(node) ⇒ Boolean

Returns:

  • (Boolean)


299
300
301
# File 'lib/wcc/scripture/esv.rb', line 299

def verse_marker?(node)
  node.name == 'marker' && node.attributes['class'].value == 'begin-verse'
end