PageRenderTime 65ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/src/core/bcv_passage.coffee

https://github.com/davidortinau/Bible-Passage-Reference-Parser
CoffeeScript | 774 lines | 556 code | 40 blank | 178 comment | 186 complexity | 9d2d915af10720c425e50b34a6345b5a MD5 | raw file
  1. # This class takes the output from the grammar and turns it into simpler objects for additional processing or for output.
  2. class bcv_passage
  3. books: []
  4. indices: {}
  5. # `bcv_parser` sets these two.
  6. options: {}
  7. translations: {}
  8. # ## Public
  9. # Loop through the parsed passages.
  10. handle_array: (passages, accum=[], context={}) ->
  11. # `passages` is an array of passage objects.
  12. for passage in passages
  13. # Each `passage` consists of passage objects and, possibly, strings.
  14. break if passage.type is "stop"
  15. [accum, context] = @handle_obj passage, accum, context
  16. [accum, context]
  17. # Handle a typical passage object with an `index`, `type`, and array in `value`.
  18. handle_obj: (passage, accum, context) ->
  19. if passage.type? and @[passage.type]?
  20. @[passage.type] passage, accum, context
  21. else [accum, context]
  22. # ## Types Returned from the Grammar
  23. # These functions correspond to `type` attributes returned from the grammar. They're designed to be called multiple times if necessary.
  24. #
  25. # Handle a book on its own.
  26. b: (passage, accum, context) ->
  27. passage.start_context = bcv_utils.shallow_clone context
  28. passage.passages = []
  29. alternates = []
  30. for b in @books[passage.value].parsed
  31. valid = @validate_ref passage.start_context.translations, {b: b}
  32. obj = start: {b: b}, end: {b: b}, valid: valid
  33. # Use the first valid book.
  34. if passage.passages.length is 0 and valid.valid
  35. passage.passages.push obj
  36. else
  37. alternates.push obj
  38. # If none are valid, use the first one.
  39. passage.passages.push alternates.shift() if passage.passages.length is 0
  40. passage.passages[0].alternates = alternates if alternates.length > 0
  41. passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
  42. passage.absolute_indices ?= @get_absolute_indices passage.indices
  43. accum.push passage
  44. context = b: passage.passages[0].start.b
  45. context.translations = passage.start_context.translations if passage.start_context.translations?
  46. [accum, context]
  47. # Handle book-only ranges.
  48. b_range: (passage, accum, context) ->
  49. @range passage, accum, context
  50. # Handle book-only ranges like 1-2 Samuel. It doesn't support multiple ambiguous ranges (like `1-2C`), which it probably shouldn't, anyway.
  51. b_range_pre: (passage, accum, context) ->
  52. passage.start_context = bcv_utils.shallow_clone context
  53. passage.passages = []
  54. alternates = []
  55. book = @pluck "b", passage.value
  56. [[end], context] = @b book, [], context
  57. passage.absolute_indices ?= @get_absolute_indices passage.indices
  58. start_obj = b: passage.value[0].value + end.passages[0].start.b.substr(1), type: "b"
  59. passage.passages = [start: start_obj, end: end.passages[0].end, valid: end.passages[0].valid]
  60. passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
  61. accum.push passage
  62. [accum, context]
  63. # The base (root) object in the grammar and controls the base indices.
  64. base: (passage, accum, context) ->
  65. @indices = @calculate_indices passage.match, passage.start_index
  66. @handle_array passage.value, accum, context
  67. # Handle book-chapter.
  68. bc: (passage, accum, context) ->
  69. passage.start_context = bcv_utils.shallow_clone context
  70. passage.passages = []
  71. for type in ["b", "c", "v"]
  72. delete context[type]
  73. c = @pluck("c", passage.value).value
  74. alternates = []
  75. for b in @books[@pluck("b", passage.value).value].parsed
  76. context_key = "c"
  77. valid = @validate_ref passage.start_context.translations, {b: b, c: c}
  78. obj = start: {b: b}, end: {b: b}, valid: valid
  79. # Is it really a `bv` object?
  80. if valid.messages.start_chapter_not_exist_in_single_chapter_book
  81. obj.valid = @validate_ref passage.start_context.translations, {b: b, v: c}
  82. obj.start.c = 1
  83. obj.end.c = 1
  84. context_key = "v"
  85. obj.start[context_key] = c
  86. # If it's zero, fix it before assigning the end.
  87. [obj.start.c, obj.start.v] = @fix_start_zeroes obj.valid, obj.start.c, obj.start.v
  88. obj.end[context_key] = obj.start[context_key]
  89. if passage.passages.length is 0 and obj.valid.valid
  90. passage.passages.push obj
  91. else
  92. alternates.push obj
  93. passage.passages.push alternates.shift() if passage.passages.length is 0
  94. passage.passages[0].alternates = alternates if alternates.length > 0
  95. passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
  96. passage.absolute_indices ?= @get_absolute_indices passage.indices
  97. for type in ["b", "c", "v"]
  98. context[type] = passage.passages[0].start[type] if passage.passages[0].start[type]?
  99. accum.push passage
  100. [accum, context]
  101. # Handle "Ps 3 title"
  102. bc_title: (passage, accum, context) ->
  103. passage.start_context = bcv_utils.shallow_clone context
  104. # First, check to see whether we're dealing with Psalms. If not, treat it as a straight `bc`.
  105. [[bc], context] = @bc @pluck("bc", passage.value), [], context
  106. if bc.passages[0].start.b isnt "Ps" and bc.passages[0].alternates?
  107. for i in [0...bc.passages[0].alternates.length]
  108. continue unless bc.passages[0].alternates[i].start.b is "Ps"
  109. # If Psalms is one of the alternates, promote it to the primary passage and discard the others--we know it's right.
  110. bc.passages[0] = bc.passages[0].alternates[i]
  111. break
  112. if bc.passages[0].start.b isnt "Ps"
  113. accum.push bc
  114. return [accum, context]
  115. # Overwrite all the other book possibilities; the presence of "title" indicates a Psalm.
  116. @books[@pluck("b", bc.value).value].parsed = ["Ps"]
  117. # Set the `indices` of the new `v` object to the indices of the `title`. We won't actually use these indices anywhere.
  118. title = @pluck "title", passage.value
  119. passage.value[1] = {type: "v", value: [{type: "integer", value: 1, indices: title.indices}], indices: title.indices}
  120. # Not for reparsing but in case we're curious later.
  121. passage.original_type = "bc_title"
  122. passage.type = "bcv"
  123. # Treat it as a standard `bcv`.
  124. @bcv passage, accum, passage.start_context
  125. # Handle book chapter:verse.
  126. bcv: (passage, accum, context) ->
  127. passage.start_context = bcv_utils.shallow_clone context
  128. passage.passages = []
  129. for type in ["b", "c", "v"]
  130. delete context[type]
  131. bc = @pluck "bc", passage.value
  132. c = @pluck("c", bc.value).value
  133. v = @pluck("v", passage.value).value
  134. alternates = []
  135. for b in @books[@pluck("b", bc.value).value].parsed
  136. valid = @validate_ref passage.start_context.translations, {b: b, c: c, v: v}
  137. [c, v] = @fix_start_zeroes valid, c, v
  138. obj = start: {b: b, c: c, v: v}, end: {b: b, c: c, v: v}, valid: valid
  139. # Use the first valid option.
  140. if passage.passages.length is 0 and valid.valid
  141. passage.passages.push obj
  142. else
  143. alternates.push obj
  144. # If there are no valid options, use the first one.
  145. passage.passages.push alternates.shift() if passage.passages.length is 0
  146. passage.passages[0].alternates = alternates if alternates.length > 0
  147. passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
  148. passage.absolute_indices ?= @get_absolute_indices passage.indices
  149. # Set all the available context keys.
  150. for type in ["b", "c", "v"]
  151. context[type] = passage.passages[0].start[type] if passage.passages[0].start[type]?
  152. accum.push passage
  153. [accum, context]
  154. # Handle "Philemon verse 6." This is unusual.
  155. bv: (passage, accum, context) ->
  156. passage.start_context = bcv_utils.shallow_clone context
  157. [b, v] = passage.value
  158. # Construct a virtual BCV object with a chapter of 1.
  159. bcv =
  160. indices: passage.indices
  161. value: [
  162. {type: "bc", value: [b, {type: "c", value: [{type: "integer", value: 1}]}]}
  163. v
  164. ]
  165. [[bcv], context] = @bcv bcv, [], context
  166. passage.passages = bcv.passages
  167. passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
  168. passage.absolute_indices ?= @get_absolute_indices passage.indices
  169. accum.push passage
  170. [accum, context]
  171. # Handle a chapter.
  172. c: (passage, accum, context) ->
  173. passage.start_context = bcv_utils.shallow_clone context
  174. # If it's an actual chapter object, the value we want is in the integer object inside it.
  175. c = if passage.type is "integer" then passage.value else @pluck("integer", passage.value).value
  176. valid = @validate_ref passage.start_context.translations, {b: context.b, c: c}
  177. # If it's a single-chapter book, then treat it as a verse even if it looks like a chapter (unless its value is `1`).
  178. if not valid.valid and valid.messages.start_chapter_not_exist_in_single_chapter_book
  179. return @v passage, accum, context
  180. [c] = @fix_start_zeroes valid, c
  181. passage.passages = [start: {b: context.b, c: c}, end: {b: context.b, c: c}, valid: valid]
  182. passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
  183. accum.push passage
  184. context.c = c
  185. delete context.v
  186. passage.absolute_indices ?= @get_absolute_indices passage.indices
  187. [accum, context]
  188. # Handle "23rd Psalm" by recasting it as a `bc`.
  189. c_psalm: (passage, accum, context) ->
  190. passage.original_type = passage.type
  191. passage.original_value = passage.value
  192. passage.type = "bc"
  193. # This string always starts with the chapter number, followed by other letters.
  194. c = @books[passage.value].value.match(/^\d+/)[0]
  195. passage.value = [
  196. {type: "b", value: passage.original_value, indices: passage.indices}
  197. {type: "c", value: [{type: "integer", value: c, indices: passage.indices}], indices: passage.indices}
  198. ]
  199. @bc passage, accum, context
  200. # Handle "Ps 3, ch 4:title"
  201. c_title: (passage, accum, context) ->
  202. passage.start_context = bcv_utils.shallow_clone context
  203. # If it's not a Psalm, treat it as a regular chapter.
  204. if context.b isnt "Ps"
  205. return @c passage.value[0], accum, context
  206. # Add a `v` object and treat it as a refular `cv`.
  207. title = @pluck "title", passage.value
  208. passage.value[1] = {type: "v", value: [{type: "integer", value: 1, indices: title.indices}], indices: title.indices}
  209. # Not for reparsing but in case we're curious later.
  210. passage.original_type = "c_title"
  211. passage.type = "cv"
  212. @cv passage, accum, passage.start_context
  213. # Handle a chapter:verse.
  214. cv: (passage, accum, context) ->
  215. passage.start_context = bcv_utils.shallow_clone context
  216. c = @pluck("c", passage.value).value
  217. v = @pluck("v", passage.value).value
  218. valid = @validate_ref passage.start_context.translations, {b: context.b, c: c, v: v}
  219. [c, v] = @fix_start_zeroes valid, c, v
  220. passage.passages = [start: {b: context.b, c: c, v: v}, end: {b: context.b, c: c, v: v}, valid: valid]
  221. passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
  222. accum.push passage
  223. context.c = c
  224. context.v = v
  225. passage.absolute_indices ?= @get_absolute_indices passage.indices
  226. [accum, context]
  227. # Handle "Chapters 1-2 from Daniel".
  228. cb_range: (passage, accum, context) ->
  229. passage.original_type = passage.type
  230. passage.type = "range"
  231. [b, start_c, end_c] = passage.value
  232. passage.original_value = [b, start_c, end_c]
  233. passage.value = [{type: "bc", value:[b, start_c], indices: passage.indices}, end_c]
  234. end_c.indices[1] = passage.indices[1]
  235. @range passage, accum, context
  236. # Handle "23rd Psalm verse 1" by recasting it as a `bcv`.
  237. cv_psalm: (passage, accum, context) ->
  238. passage.start_context = bcv_utils.shallow_clone context
  239. passage.original_type = passage.type
  240. passage.original_value = passage.value
  241. [c_psalm, v] = passage.value
  242. passage.type = "bcv"
  243. [[bc]] = @c_psalm c_psalm, [], passage.start_context
  244. passage.value = [bc, v]
  245. @bcv passage, accum, context
  246. # Handle "and following" (e.g., "Matt 1:1ff") by assuming it means to continue to the end of the current context (end of chapter if a verse is given, end of book if a chapter is given).
  247. ff: (passage, accum, context) ->
  248. passage.start_context = bcv_utils.shallow_clone context
  249. # Create a virtual end to pass to `@range`.
  250. passage.value.push type: "integer", indices: passage.indices, value: 999
  251. [[passage], context] = @range passage, [], passage.start_context
  252. # And then get rid of the virtual end so it doesn't stick around if we need to reparse it later.
  253. passage.value.pop()
  254. # Ignore any warnings that the end chapter / verse doesn't exist.
  255. delete passage.passages[0].valid.end_verse_not_exist if passage.passages[0].valid.end_verse_not_exist?
  256. delete passage.passages[0].valid.end_chapter_not_exist if passage.passages[0].valid.end_chapter_not_exist?
  257. delete passage.passages[0].end.original_c if passage.passages[0].end.original_c?
  258. # `translations` was handled in `@range`.
  259. accum.push passage
  260. passage.absolute_indices ?= @get_absolute_indices passage.indices
  261. [accum, context]
  262. # Handle "Ps 3-4:title" or "Acts 2:22-27. Title"
  263. integer_title: (passage, accum, context) ->
  264. passage.start_context = bcv_utils.shallow_clone context
  265. # If it's not Psalms, treat it as a straight integer, ignoring the "title".
  266. if context.b isnt "Ps"
  267. return @integer passage.value[0], accum, context
  268. passage.value[0] = type: "c", value: [passage.value[0]], indices: [passage.value[0].indices[0], passage.value[0].indices[1]]
  269. # Add a `v` object.
  270. v_indices = [passage.indices[1] - 5, passage.indices[1]]
  271. passage.value[1] = {type: "v", value: [{type: "integer", value: 1, indices: v_indices}], indices: v_indices}
  272. # Not for reparsing but in case we're curious later.
  273. passage.original_type = "integer_title"
  274. passage.type = "cv"
  275. @cv passage, accum, passage.start_context
  276. # Pass the integer off to whichever handler is relevant.
  277. integer: (passage, accum, context) ->
  278. return @v passage, accum, context if context.v?
  279. return @c passage, accum, context
  280. # Handle a sequence of references. This is the only function that can return more than one object in the `passage.passages` array.
  281. sequence: (passage, accum, context) ->
  282. passage.start_context = bcv_utils.shallow_clone context
  283. passage.passages = []
  284. for obj in passage.value
  285. [[psg], context] = @handle_array obj, [], context
  286. # There's only more than one `sub_psg` if there was a range error.
  287. for sub_psg in psg.passages
  288. sub_psg.type ?= psg.type
  289. # Add the indices so we can possibly retrieve them later, depending on our `sequence_combination_strategy`.
  290. sub_psg.absolute_indices ?= psg.absolute_indices
  291. sub_psg.translations = psg.start_context.translations if psg.start_context.translations?
  292. # Save the index of any closing punctuation if the sequence ends with a `sequence_post_enclosed`.
  293. sub_psg.enclosed_absolute_indices = if psg.type is "sequence_post_enclosed" then psg.absolute_indices else [-1, -1]
  294. passage.passages.push sub_psg
  295. unless passage.absolute_indices?
  296. # If it's `sequence_post_enclosed`, don't snap the end index; include the closing punctuation.
  297. if passage.passages.length > 0 and passage.type is "sequence"
  298. passage.absolute_indices = [passage.passages[0].absolute_indices[0], passage.passages[passage.passages.length - 1].absolute_indices[1]]
  299. else
  300. passage.absolute_indices = @get_absolute_indices passage.indices
  301. accum.push passage
  302. [accum, context]
  303. # Handle a sequence like "Ps 119 (118)," with parentheses. We want to include the closing parenthesis in the indices if `sequence_combination_strategy` is `combine` or if there's a consecutive.
  304. sequence_post_enclosed: (passage, accum, context) ->
  305. @sequence passage, accum, context
  306. # Handle a verse, either as part of a sequence or because someone explicitly wrote "verse".
  307. v: (passage, accum, context) ->
  308. v = if passage.type is "integer" then passage.value else @pluck("integer", passage.value).value
  309. passage.start_context = bcv_utils.shallow_clone context
  310. # The chapter context might not be set if it follows a book in a sequence.
  311. c = if context.c? then context.c else 1
  312. valid = @validate_ref passage.start_context.translations, {b: context.b, c: c, v: v}
  313. [no_c, v] = @fix_start_zeroes valid, 0, v
  314. passage.passages = [start: {b: context.b, c: c, v: v}, end: {b: context.b, c: c, v: v}, valid: valid]
  315. passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
  316. passage.absolute_indices ?= @get_absolute_indices passage.indices
  317. accum.push passage
  318. context.v = v
  319. [accum, context]
  320. # ## Ranges
  321. # Handle any type of start and end range.
  322. range: (passage, accum, context) ->
  323. passage.start_context = bcv_utils.shallow_clone context
  324. [start, end] = passage.value
  325. # Matt 5-verse 6 = Matt.5.6
  326. if end.type is "v" and (start.type is "bc" or start.type is "c") and @options.end_range_digits_strategy is "verse"
  327. return @range_change_integer_end passage, accum
  328. # These always return exactly one object that we're interested in.
  329. [[start], context] = @handle_obj start, [], context
  330. [[end], context] = @handle_obj end, [], context
  331. # If we had to change the start or end `type`s, make sure that's reflected in the `value`.
  332. passage.value = [start, end]
  333. # Similarly, if we had to adjust the indices, make sure they're reflected in the indices for the range.
  334. passage.indices = [start.indices[0], end.indices[1]]
  335. # We'll also need to recalculate these if they exist.
  336. delete passage.absolute_indices
  337. # Create the prospective start and end objects that will end up in `passage.passages`.
  338. start_obj = b: start.passages[0].start.b, c: start.passages[0].start.c, v: start.passages[0].start.v, type: start.type
  339. end_obj = b: end.passages[0].end.b, c: end.passages[0].end.c, v: end.passages[0].end.v, type: end.type
  340. end_obj.c = 0 if end.passages[0].valid.messages.start_chapter_is_zero
  341. end_obj.v = 0 if end.passages[0].valid.messages.start_verse_is_zero
  342. valid = @validate_ref passage.start_context.translations, start_obj, end_obj
  343. if valid.valid
  344. # If Heb 13-15, treat it as Heb 13:15. This may be too clever for its own good.
  345. if valid.messages.end_chapter_not_exist and @options.end_range_digits_strategy is "verse" and not start_obj.v? and (end.type is "integer" or end.type is "v")
  346. temp_value = if end.type is "v" then @pluck "integer", end.value else end.value
  347. temp_valid = @validate_ref passage.start_context.translations, {b: start_obj.b, c: start_obj.c, v: temp_value}
  348. return @range_change_integer_end passage, accum if temp_valid.valid
  349. # If "John 10:22-42 vs 27", we're possibly misreading the "42 vs 27" as a `cv`.
  350. if valid.messages.end_chapter_not_exist and @options.end_range_digits_strategy is "verse" and start_obj.v? and end.type is "cv"
  351. # Make sure that what we're changing it to actually exists (that the chapter number can become the verse number, and the verse number is also valid in the current chapter).
  352. temp_valid = @validate_ref passage.start_context.translations, {b:end_obj.b, c: start_obj.c, v: end_obj.c}
  353. temp_valid = @validate_ref passage.start_context.translations, {b:end_obj.b, c: start_obj.c, v: end_obj.v} if temp_valid.valid
  354. return @range_change_cv_end passage, accum if temp_valid.valid
  355. # Otherwise, snap start/end chapters/verses if they're too high or low.
  356. @range_validate valid, start_obj, end_obj, passage
  357. else
  358. # Is it not valid because the end is before the start and the `end` is an `integer` (Matt 15-6) or a `cv` (Matt 15-6:2) (since anything else resets our expectations)?
  359. #
  360. # Only go with a `cv` if it's the chapter that's too low (to avoid doing weird things with 31:30-31:1).
  361. if ((valid.messages.end_chapter_before_start or valid.messages.end_verse_before_start) and (end.type is "integer" or end.type is "v") or (valid.messages.end_chapter_before_start and end.type is "cv"))
  362. new_end = @range_check_new_end passage.start_context.translations, start_obj, end_obj, valid
  363. # If that's the case, then reparse the current passage object after correcting the end value, which is an integer.
  364. return @range_change_end passage, accum, new_end if new_end > 0
  365. # If someone enters "Jer 33-11", they probably mean "Jer.33.11"; as above, this may be too clever for its own good.
  366. if @options.end_range_digits_strategy is "verse" and start_obj.v is undefined and (end.type is "integer" or end.type is "v")
  367. temp_value = if end.type is "v" then @pluck "integer", end.value else end.value
  368. temp_valid = @validate_ref passage.start_context.translations, {b: start_obj.b, c: start_obj.c, v: temp_value}
  369. return @range_change_integer_end passage, accum if temp_valid.valid
  370. # Otherwise, if we couldn't fix the range, then treat the range as a sequence.
  371. [passage.original_type, passage.type] = [passage.type, "sequence"]
  372. # Construct the sequence value in the format expected.
  373. [passage.original_value, passage.value] = [[start, end], [[start], [end]]]
  374. # Don't use the `context` object because we've changed it in this function.
  375. return @handle_obj passage, accum, passage.start_context
  376. # We've already reset the indices to match the indices of the contained objects.
  377. passage.absolute_indices ?= @get_absolute_indices passage.indices
  378. passage.passages = [start: start_obj, end: end_obj, valid: valid]
  379. passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
  380. accum.push passage
  381. [accum, context]
  382. # For Ps 122-23, treat the 23 as 123.
  383. range_change_end: (passage, accum, new_end) ->
  384. [start, end] = passage.value
  385. if end.type is "integer"
  386. end.original_value = end.value
  387. end.value = new_end
  388. else if end.type is "v"
  389. new_obj = @pluck "integer", end.value
  390. new_obj.original_value = new_obj.value
  391. new_obj.value = new_end
  392. else if end.type is "cv"
  393. # Get the chapter object and assign it (in place) the new value.
  394. new_obj = @pluck "c", end.value
  395. new_obj.original_value = new_obj.value
  396. new_obj.value = new_end
  397. @handle_obj passage, accum, passage.start_context
  398. # For "Jer 33-11", treat the "11" as a verse.
  399. range_change_integer_end: (passage, accum) ->
  400. [start, end] = passage.value
  401. passage.original_type = passage.type
  402. passage.original_value = [start, end]
  403. # The start.type is only bc, c, or integer; we're just adding a v for the first two.
  404. passage.type = if start.type is "integer" then "cv" else start.type + "v"
  405. # Create the object in the expected format if it's not already a verse.
  406. passage.value[0] = {type: "c", value: [start], indices: start.indices} if start.type is "integer"
  407. passage.value[1] = {type: "v", value: [end], indices: end.indices} if end.type is "integer"
  408. @handle_obj passage, accum, passage.start_context
  409. # In cases like "John 10:22-42 vs 27", treat it as "10:22-42,47" instead of "10:22-42:27".
  410. range_change_cv_end: (passage, accum) ->
  411. [start, end] = passage.value
  412. passage.original_type = passage.type
  413. passage.original_value = [start, end]
  414. passage.type = "sequence"
  415. [new_range_end, new_sequence_end] = end.value
  416. # If a translation sequence needs to come back here and reuse it, make sure it can get the old object (`end`)--it only looks in accum, not in deep objects.
  417. new_range_end = bcv_utils.shallow_clone new_range_end
  418. # Was "c" but change it to "v" to serve as the end of a range.
  419. new_range_end.original_type = new_range_end.type
  420. new_range_end.type = "v"
  421. # Change it into a sequence consisting of a range and a free verse.
  422. passage.value = [
  423. [{type: "range", value: [start, new_range_end], indices: [start.indices[0], new_range_end.indices[1]]}]
  424. [new_sequence_end]
  425. ]
  426. @sequence passage, accum, passage.start_context
  427. range_validate: (valid, start_obj, end_obj, passage) ->
  428. # If it's valid but the end range goes too high, snap it back to the appropriate chapter or verse.
  429. if valid.messages.end_chapter_not_exist
  430. # `end_chapter_not_exist` gives the highest chapter for the book.
  431. end_obj.original_c = end_obj.c
  432. end_obj.c = valid.messages.end_chapter_not_exist
  433. # If we've snapped it back to the last chapter and there's a verse, also snap to the end of that chapter. If we've already overshot the chapter, there's no reason to think we've gotten the verse right; Gen 50:1-51:1 = Gen 50:1-26 = Gen 50. If there's no verse, we don't need to worry about it.
  434. if end_obj.v?
  435. # `end_verse_not_exist` gives the maximum verse for the chapter.
  436. end_obj.v = @validate_ref(passage.start_context.translations, {b: end_obj.b, c: end_obj.c, v: 999}).messages.end_verse_not_exist
  437. # If the end verse is too high, snap back to the maximum verse.
  438. else if valid.messages.end_verse_not_exist
  439. end_obj.original_v = end_obj.v
  440. end_obj.v = valid.messages.end_verse_not_exist
  441. end_obj.v = valid.messages.end_verse_is_zero if valid.messages.end_verse_is_zero and @options.zero_verse_strategy isnt "allow"
  442. end_obj.c = valid.messages.end_chapter_is_zero if valid.messages.end_chapter_is_zero
  443. [start_obj.c, start_obj.v] = @fix_start_zeroes valid, start_obj.c, start_obj.v
  444. true
  445. # If the start chapter or verse is 0, convert it to a 1. `valid.valid` is `false` if the `zero_*_strategy` is `error`.
  446. fix_start_zeroes: (valid, c, v) ->
  447. if valid.valid
  448. c = valid.messages.start_chapter_is_zero if valid.messages.start_chapter_is_zero
  449. v = valid.messages.start_verse_is_zero if valid.messages.start_verse_is_zero and @options.zero_verse_strategy isnt "allow"
  450. [c, v]
  451. # If a new end chapter/verse in a range may be necessary, calculate it.
  452. range_check_new_end: (translations, start_obj, end_obj, valid) ->
  453. new_end = 0
  454. type = null
  455. # See whether a digit might be omitted (e.g., Gen 22-4 = Gen 22-24).
  456. if valid.messages.end_chapter_before_start then type = "c"
  457. else if valid.messages.end_verse_before_start then type = "v"
  458. new_end = @range_get_new_end_value(start_obj, end_obj, valid, type) if type?
  459. if new_end > 0
  460. obj_to_validate = b: end_obj.b, c: end_obj.c, v: end_obj.v
  461. obj_to_validate[type] = new_end
  462. new_valid = @validate_ref translations, obj_to_validate
  463. new_end = 0 unless new_valid.valid
  464. new_end
  465. # If a sequence has an end chapter/verse that's before the the start, check to see whether it can be salvaged: Gen 28-9 = Gen 28-29; Ps 101-24 = Ps 101-124. The `key` parameter is either `c` (for chapter) or `v` (for verse).
  466. range_get_new_end_value: (start_obj, end_obj, valid, key) ->
  467. # Return 0 unless it's salvageable.
  468. new_end = 0
  469. return new_end if ((key is "c" and valid.messages.end_chapter_is_zero) or (key is "v" and valid.messages.end_verse_is_zero))
  470. # 54-5, not 54-43, 54-3, or 54-4.
  471. if start_obj[key] >= 10 and end_obj[key] < 10 and start_obj[key] - 10 * Math.floor(start_obj[key] / 10) < end_obj[key]
  472. # Add the start tens digit to the original end value: 54-5 = 54 through 50 + 5.
  473. new_end = end_obj[key] + 10 * Math.floor(start_obj[key] / 10)
  474. # 123-40, not 123-22 or 123-23; 123-4 is taken care of in the first case.
  475. else if start_obj[key] >= 100 and end_obj[key] < 100 and start_obj[key] - 100 < end_obj[key]
  476. # Add 100 to the original end value: 100-12 = 100 through 100 + 12.
  477. new_end = end_obj[key] + 100
  478. new_end
  479. # ## Translations
  480. # Even a single translation ("NIV") appears as part of a translation sequence. Here we handle the sequence and apply the translations to any previous passages lacking an explicit translation: in "Matt 1, 5 ESV," both `Matt 1` and `5` get applied, but in "Matt 1 NIV, 5 ESV," NIV only applies to Matt 1, and ESV only applies to Matt 5.
  481. translation_sequence: (passage, accum, context) ->
  482. translations = []
  483. # First get all the translations in the sequence; the first one is separate from the others (which may not exist).
  484. translations.push translation: @books[passage.value[0].value].parsed
  485. for val in passage.value[1]
  486. # `val` at this point is an array.
  487. val = @books[@pluck("translation", val).value].parsed
  488. # And now `val` is the literal, lower-cased match.
  489. translations.push translation: val if val?
  490. # We need some metadata to do this right.
  491. for translation in translations
  492. # Do we know anything about this translation? If so, use that. If not, use the default.
  493. if @translations.aliases[translation.translation]?
  494. # `alias` is what we use internally to get bcv data for the translation.
  495. translation.alias = @translations.aliases[translation.translation].alias
  496. # `osis` is what we'll eventually use in output.
  497. translation.osis = @translations.aliases[translation.translation].osis
  498. else
  499. translation.alias = "default"
  500. # If we don't know what the correct abbreviation should be, then just upper-case what we have.
  501. translation.osis = translation.translation.toUpperCase()
  502. # Now we need to go back and find the earliest already-parsed passage without a translation. We start with 0 because the below loop will never yield a 0.
  503. if accum.length > 0
  504. use_i = 0
  505. # Start with the most recent and go backward--we don't want to overlap another `translation_sequence`.
  506. for i in [accum.length - 1 .. 0]
  507. # With a new translation comes the possibility that a previously invalid reference will become valid, so reset it to its original type. For example, a multi-book range may be correct in a different translation because the books are in a different order.
  508. accum[i].type = accum[i].original_type if accum[i].original_type?
  509. accum[i].value = accum[i].original_value if accum[i].original_value?
  510. continue unless accum[i].type == "translation_sequence"
  511. # If we made it here, then we hit a translation sequence, and we know that the item following it is the first one we care about.
  512. use_i = i + 1
  513. break
  514. # Include the translations in the start context.
  515. #
  516. # `use_i` == `accum.length` if there are two translations sequences in a row separated by, e.g., numbers ("Matt 1 ESV 2-3 NIV").
  517. if use_i < accum.length
  518. accum[use_i].start_context.translations = translations
  519. # The objects in accum are replaced in-place, so we don't need to try to merge them back. We re-parse them because the translation may cause previously valid (or invalid) references to flip the other way--if the new translation includes (or doesn't) the Deuterocanonicals, for example. We ignore the `new_accum`, but we definitely care about the new `context`.
  520. [new_accum, context] = @handle_array accum.slice(use_i), [], accum[use_i].start_context
  521. # We may need these indices later, depending on how we want to output the data.
  522. passage.absolute_indices ?= @get_absolute_indices passage.indices
  523. # Include the `translation_sequence` object so that we can handle any later `translation_sequence` objects without overlapping this one.
  524. accum.push passage
  525. # Don't carry over the translations into any later references; translations only apply backwards.
  526. delete context.translations
  527. [accum, context]
  528. # ## Utilities
  529. # Pluck the object or value matching a type from an array.
  530. pluck: (type, passages) ->
  531. for passage in passages
  532. continue unless passage.type? and passage.type is type
  533. return @pluck("integer", passage.value) if type is "c" or type is "v"
  534. return passage
  535. null
  536. # Given a string and initial index, calculate indices for parts of the string. For example, a string that starts at index 10 might have a book that pushes it to index 12 starting at its third character.
  537. calculate_indices: (match, adjust) ->
  538. # This gets switched out the first time in the loop; the first item is never a book even if a book is the first part of the string--there's an empty string before it.
  539. switch_type = "book"
  540. indices = []
  541. match_index = 0
  542. adjust = parseInt adjust, 10
  543. # It would be easier to do `for part in match.split /[\x1e\x1f]/`, but IE doesn't return empty matches when using `split`, throwing off the rest of the logic.
  544. parts = [match]
  545. for character in ["\x1e", "\x1f"]
  546. temp = []
  547. for part in parts
  548. temp = temp.concat part.split(character)
  549. parts = temp
  550. for part in parts
  551. # Start off assuming it's not a book.
  552. switch_type = if switch_type is "book" then "rest" else "book"
  553. # Empty strings don't move the index. This could happen with consecutive books.
  554. part_length = part.length
  555. continue if part_length == 0
  556. # If it's a book, then get the start index of the actual book, add the length of the actual string, then subtract the length of the integer id and the two surrounding characters.
  557. if switch_type is "book"
  558. # Remove any stray extra indicators.
  559. part = part.replace /\/[a-z]$/, ""
  560. # Get the length of the id + the surrounding characters. We want the `end` to be the position, not the length. If the part starts at position 0 and is one character (i.e., three characters total, or `\x1f0\x1f`), `end` should be 1, since it occupies positions 0, 1, and 2, and we want the last character to be part of the next index so that we keep track of the end. For example, with "Genesis" at start index 0, the index starting at position 6 ("s") should be 4. Keep the adjust as-is, but set it next.
  561. end_index = match_index + part_length
  562. if indices.length > 0 and indices[indices.length - 1].index == adjust
  563. indices[indices.length - 1].end = end_index
  564. else
  565. indices.push start: match_index, end: end_index, index: adjust
  566. # If the part is one character (three characters total) starting at `match_index` 0, we want the next `match_index` to be 3; it occupies positions 0, 1, and 2. Similarly, if it's two characters, it should be four characters total.
  567. match_index += part_length + 2
  568. # Use the known `start_index` from the book, subtracting the current index in the match, to get the new. So if the previous `match_index` == 5 and the book's id is 0, the book's `start_index` == 10, and the book's length == 7, we want the next adjust to be 10 + 7 - 8 = 9 (the 8 is the `match_index` where the new `adjust` starts): 4(+5) = 9, 5(+5) = 10, 6(+5) = 11, 7(+5) = 12, 8(+9) = 17.
  569. adjust = @books[part].start_index + @books[part].value.length - match_index
  570. indices.push start: end_index + 1, end: end_index + 1, index: adjust
  571. else
  572. # The `- 1` is because we want the `end` to be the position of the last character. If the part starts at position 0 and is three characters long, the `end` should be two, since it occupies positions 0, 1, and 2.
  573. end_index = match_index + part_length - 1
  574. if indices.length > 0 and indices[indices.length - 1].index == adjust
  575. indices[indices.length - 1].end = end_index
  576. else
  577. indices.push start: match_index, end: end_index, index: adjust
  578. match_index += part_length
  579. indices
  580. # Find the absolute string indices of start and end points.
  581. get_absolute_indices: ([start, end]) ->
  582. start_out = null
  583. end_out = null
  584. # `@indices` contains the absolute indices for each range of indices in the string.
  585. for index in @indices
  586. # If we haven't found the absolute start index yet, set it.
  587. if start_out is null and index.start <= start <= index.end
  588. start_out = start + index.index
  589. # This may be in the same loop iteration as `start`. The `+ 1` matches Twitter's implementation of indices, where start is the character index and end is the character after the index. So `Gen` is `[0, 3]`.
  590. if index.start <= end <= index.end
  591. end_out = end + index.index + 1
  592. break
  593. [start_out, end_out]
  594. # ## Validators
  595. # Given a start and optional end bcv object, validate that the verse exists and is valid. It returns an array with validity for each translations.
  596. validate_ref: (translations, start, end) ->
  597. # The `translation` key is optional; if it doesn't exist, assume the default translation.
  598. translations or= [{translation: "default", osis: "", alias: "default"}]
  599. translation = translations[0]
  600. # Only true if `translations` isn't the right type.
  601. return {valid: false, messages: {translation_invalid: true}} unless translation?
  602. valid = true
  603. messages = {}
  604. # `translation` is a translation object, but all we care about is the string.
  605. translation.alias ?= "default"
  606. # Only true if `translations` isn't the right type.
  607. return {valid: false, messages: {translation_invalid: true}} unless translation.alias?
  608. # Not a fatal error because we assume that translations match the default unless we know differently. But we still record it because we may want to know about it later. Translations in `alternates` get generated on-demand.
  609. unless @translations.aliases[translation.alias]?
  610. translation.alias = "default"
  611. messages.translation_unknown = true
  612. [valid, messages] = @validate_start_ref translation.alias, start, valid, messages
  613. [valid, messages] = @validate_end_ref translation.alias, start, end, valid, messages if end
  614. valid: valid, messages: messages
  615. # Make sure that the start ref exists in the given translation.
  616. validate_start_ref: (translation, start, valid, messages) ->
  617. if translation isnt "default" and !@translations[translation]?.chapters[start.b]?
  618. @promote_book_to_translation start.b, translation
  619. translation_order = if @translations[translation]?.order? then translation else "default"
  620. # Matt
  621. if @translations[translation_order].order[start.b]?
  622. start.c ?= 1
  623. start.c = parseInt start.c, 10
  624. # Matt five
  625. if isNaN start.c
  626. valid = false
  627. messages.start_chapter_not_numeric = true
  628. return [valid, messages]
  629. # Matt 0
  630. if start.c == 0
  631. messages.start_chapter_is_zero = 1
  632. if @options.zero_chapter_strategy is "error" then valid = false
  633. else start.c = 1
  634. # Matt 5
  635. if start.c > 0 and @translations[translation].chapters[start.b][start.c - 1]?
  636. # Matt 5:10
  637. if start.v?
  638. start.v = parseInt start.v, 10
  639. # Matt 5:ten
  640. if isNaN start.v
  641. valid = false
  642. messages.start_verse_not_numeric = true
  643. # Matt 5:0
  644. else if start.v == 0
  645. messages.start_verse_is_zero = 1
  646. if @options.zero_verse_strategy is "error" then valid = false
  647. else if @options.zero_verse_strategy is "upgrade" then start.v = 1
  648. # Matt 5:100
  649. else if start.v > @translations[translation].chapters[start.b][start.c - 1]
  650. valid = false
  651. messages.start_verse_not_exist = @translations[translation].chapters[start.b][start.c - 1]
  652. # Matt 50
  653. else
  654. valid = false
  655. if start.c != 1 and @translations[translation].chapters[start.b].length == 1
  656. messages.start_chapter_not_exist_in_single_chapter_book = 1
  657. else if start.c > 0
  658. messages.start_chapter_not_exist = @translations[translation].chapters[start.b].length
  659. # None 2:1
  660. else
  661. valid = false
  662. messages.start_book_not_exist = true
  663. [valid, messages]
  664. # The end ref pretty much just has to be after the start ref; beyond the book, we don't require that the chapter or verse exists. This is useful when people get end verses wrong.
  665. validate_end_ref: (translation, start, end, valid, messages) ->
  666. if translation isnt "default" and !@translations[translation]?.chapters[end.b]?
  667. @promote_book_to_translation end.b, translation
  668. translation_order = if @translations[translation]?.order? then translation else "default"
  669. end.c = parseInt end.c, 10 if end.c?
  670. end.v = parseInt end.v, 10 if end.v?
  671. # Matt 0
  672. if end.c? and not(isNaN end.c) and end.c == 0
  673. messages.end_chapter_is_zero = 1
  674. if @options.zero_chapter_strategy is "error" then valid = false
  675. else end.c = 1
  676. # Matt-Mark
  677. if @translations[translation_order].order[end.b]?
  678. # Mark 4-Matt 5, None 4-Matt 5
  679. if @translations[translation_order].order[start.b]? and @translations[translation_order].order[start.b] > @translations[translation_order].order[end.b]
  680. valid = false
  681. messages.end_book_before_start = true
  682. # Matt 5-6
  683. if start.b == end.b and end.c? and not isNaN end.c
  684. # Matt-Matt 4
  685. start.c ?= 1
  686. # Matt 5-4
  687. if not isNaN(parseInt start.c, 10) and start.c > end.c
  688. valid = false
  689. messages.end_chapter_before_start = true
  690. # Matt 5:7-5:8
  691. else if start.c == end.c and end.v? and not isNaN end.v
  692. # Matt 5-5:8
  693. start.v ?= 1
  694. # Matt 5:8-7
  695. if not isNaN(parseInt start.v, 10) and start.v > end.v
  696. valid = false
  697. messages.end_verse_before_start = true
  698. if end.c? and not isNaN end.c
  699. if not @translations[translation].chapters[end.b][end.c - 1]?
  700. if @translations[translation].chapters[end.b].length is 1
  701. messages.end_chapter_not_exist_in_single_chapter_book = 1
  702. else if end.c > 0
  703. messages.end_chapter_not_exist = @translations[translation].chapters[end.b].length
  704. if end.v? and not isNaN end.v
  705. end.c ?= @translations[translation].chapters[end.b].length
  706. if end.v > @translations[translation].chapters[end.b][end.c - 1]
  707. messages.end_verse_not_exist = @translations[translation].chapters[end.b][end.c - 1]
  708. else if end.v == 0
  709. messages.end_verse_is_zero = 1
  710. if @options.zero_verse_strategy is "error" then valid = false
  711. else if @options.zero_verse_strategy is "upgrade" then end.v = 1
  712. # Matt 5:1-None 6
  713. else
  714. valid = false
  715. messages.end_book_not_exist = true
  716. # Matt 2-four
  717. if end.c? and isNaN end.c
  718. valid = false
  719. messages.end_chapter_not_numeric = true
  720. # Matt 5:7-eight
  721. if end.v? and isNaN end.v
  722. valid = false
  723. messages.end_verse_not_numeric = true
  724. [valid, messages]
  725. # Gradually add books to translations as they're needed.
  726. promote_book_to_translation: (book, translation) ->
  727. @translations[translation] ?= {}
  728. @translations[translation].chapters ?= {}
  729. # If the translation specifically overrides the default, use that.
  730. if @translations.alternates[translation]?.chapters?[book]?
  731. @translations[translation].chapters[book] = @translations.alternates[translation].chapters[book]
  732. # Otherwise stick with the default.
  733. else
  734. @translations[translation].chapters[book] = bcv_utils.shallow_clone_array @translations.default.chapters[book]