1use std::{
4 collections::{BTreeMap, HashMap, HashSet},
5 sync::{Arc, OnceLock},
6};
7
8use crate::{
9 Book, Language, ParseError, ParseErrorKind, Reference, VerseRange, VerseRef,
10 language_data::aliases as localized_aliases,
11 normalize::{normalize as normalize_syntax, normalize_for_parsing},
12 reference::{MAX_CHAPTER_NUMBER, MAX_VERSE_NUMBER},
13};
14
15pub const AUTO_LANGUAGE_PRECEDENCE: &[Language] = &[
17 Language::Arabic,
18 Language::Chinese,
19 Language::French,
20 Language::German,
21 Language::Hebrew,
22 Language::Hindi,
23 Language::Indonesian,
24 Language::Korean,
25 Language::Portuguese,
26 Language::Russian,
27 Language::Spanish,
28 Language::Tagalog,
29];
30
31#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
33pub enum AmbiguityPolicy {
34 #[default]
36 PreferLanguagePriority,
37 Reject,
39}
40
41#[derive(Clone, Debug, Eq, Hash, PartialEq)]
43pub struct BookCandidate {
44 book: Book,
45 alias: Arc<str>,
46 language: Option<Language>,
47 custom: bool,
48}
49
50impl BookCandidate {
51 #[must_use]
53 pub const fn book(&self) -> Book {
54 self.book
55 }
56
57 #[must_use]
59 pub fn alias(&self) -> &str {
60 &self.alias
61 }
62
63 #[must_use]
67 pub const fn language(&self) -> Option<Language> {
68 self.language
69 }
70
71 #[must_use]
73 pub const fn is_custom(&self) -> bool {
74 self.custom
75 }
76}
77
78#[derive(Clone, Debug, Eq, PartialEq)]
80pub struct BookMatch {
81 token: String,
82 selected: BookCandidate,
83 alternatives: Vec<BookCandidate>,
84}
85
86impl BookMatch {
87 #[must_use]
89 pub fn token(&self) -> &str {
90 &self.token
91 }
92
93 #[must_use]
95 pub const fn selected(&self) -> &BookCandidate {
96 &self.selected
97 }
98
99 #[must_use]
101 pub fn alternatives(&self) -> &[BookCandidate] {
102 &self.alternatives
103 }
104
105 #[must_use]
107 pub fn is_ambiguous(&self) -> bool {
108 self.alternatives
109 .iter()
110 .any(|candidate| candidate.book != self.selected.book)
111 }
112}
113
114#[derive(Clone, Debug, Eq, PartialEq)]
116pub struct ParseMetadata {
117 normalized_input: String,
118 book_matches: Vec<BookMatch>,
119}
120
121impl ParseMetadata {
122 pub(crate) fn from_parts(normalized_input: String, book_matches: Vec<BookMatch>) -> Self {
123 Self {
124 normalized_input,
125 book_matches,
126 }
127 }
128
129 #[must_use]
131 pub fn normalized_input(&self) -> &str {
132 &self.normalized_input
133 }
134
135 #[must_use]
137 pub fn book_matches(&self) -> &[BookMatch] {
138 &self.book_matches
139 }
140
141 #[must_use]
143 pub fn detected_languages(&self) -> Vec<Language> {
144 let mut languages = Vec::new();
145 for language in self
146 .book_matches
147 .iter()
148 .filter_map(|book_match| book_match.selected.language)
149 {
150 if !languages.contains(&language) {
151 languages.push(language);
152 }
153 }
154 languages
155 }
156
157 #[must_use]
159 pub fn detected_language(&self) -> Option<Language> {
160 let languages = self.detected_languages();
161 match languages.as_slice() {
162 [language] => Some(*language),
163 _ => None,
164 }
165 }
166
167 #[must_use]
169 pub fn has_ambiguity(&self) -> bool {
170 self.book_matches.iter().any(BookMatch::is_ambiguous)
171 }
172
173 pub fn alternate_matches(&self) -> impl Iterator<Item = &BookCandidate> {
175 self.book_matches
176 .iter()
177 .flat_map(|book_match| book_match.alternatives.iter())
178 }
179}
180
181#[derive(Clone, Debug, Eq, PartialEq)]
182struct CustomAlias {
183 normalized: String,
184 alias: Arc<str>,
185 book: Book,
186}
187
188#[derive(Clone, Debug, Eq, PartialEq)]
190pub struct Parsed<T> {
191 value: T,
192 metadata: ParseMetadata,
193}
194
195impl<T> Parsed<T> {
196 #[must_use]
198 pub const fn new(value: T, metadata: ParseMetadata) -> Self {
199 Self { value, metadata }
200 }
201
202 #[must_use]
204 pub const fn value(&self) -> &T {
205 &self.value
206 }
207
208 #[must_use]
210 pub const fn metadata(&self) -> &ParseMetadata {
211 &self.metadata
212 }
213
214 #[must_use]
216 pub fn into_value(self) -> T {
217 self.value
218 }
219
220 #[must_use]
222 pub fn into_parts(self) -> (T, ParseMetadata) {
223 (self.value, self.metadata)
224 }
225}
226
227#[derive(Clone, Debug, Default)]
229pub struct ParserBuilder {
230 aliases: Vec<(String, Book)>,
231 aliases_by_language: Vec<(Language, String, Book)>,
232 preferred_languages: Vec<Language>,
233 ambiguity_policy: AmbiguityPolicy,
234}
235
236impl ParserBuilder {
237 #[must_use]
239 pub fn alias(mut self, alias: impl Into<String>, book: Book) -> Self {
240 self.aliases.push((alias.into(), book));
241 self
242 }
243
244 #[must_use]
246 pub fn language_alias(
247 mut self,
248 language: Language,
249 alias: impl Into<String>,
250 book: Book,
251 ) -> Self {
252 self.aliases_by_language
253 .push((language, alias.into(), book));
254 self
255 }
256
257 #[must_use]
259 pub fn preferred_languages(mut self, languages: impl IntoIterator<Item = Language>) -> Self {
260 self.preferred_languages.clear();
261 for language in languages {
262 if !self.preferred_languages.contains(&language) {
263 self.preferred_languages.push(language);
264 }
265 }
266 self
267 }
268
269 #[must_use]
271 pub const fn ambiguity_policy(mut self, policy: AmbiguityPolicy) -> Self {
272 self.ambiguity_policy = policy;
273 self
274 }
275
276 pub fn build(self) -> Result<ReferenceParser, ParseError> {
278 if self.preferred_languages.contains(&Language::Auto) {
279 return Err(ParseError::new(
280 ParseErrorKind::UnsupportedLanguage,
281 "preferred languages must be concrete languages, not auto mode",
282 ));
283 }
284 let mut aliases = Vec::new();
285 for (alias, book) in self.aliases {
286 let key = normalize_book_term(&alias);
287 if key.is_empty() {
288 return Err(ParseError::new(
289 ParseErrorKind::EmptyBookToken,
290 "custom aliases must contain a book token",
291 ));
292 }
293 aliases.push(CustomAlias {
294 normalized: key,
295 alias: Arc::from(alias),
296 book,
297 });
298 }
299
300 let mut aliases_by_language = Vec::new();
301 for (language, alias, book) in self.aliases_by_language {
302 if language.is_auto() {
303 return Err(ParseError::new(
304 ParseErrorKind::UnsupportedLanguage,
305 "language-specific aliases cannot use auto mode",
306 ));
307 }
308 let key = normalize_book_term(&alias);
309 if key.is_empty() {
310 return Err(ParseError::new(
311 ParseErrorKind::EmptyBookToken,
312 "custom aliases must contain a book token",
313 ));
314 }
315 aliases_by_language.push((
316 language,
317 CustomAlias {
318 normalized: key,
319 alias: Arc::from(alias),
320 book,
321 },
322 ));
323 }
324
325 Ok(ReferenceParser {
326 aliases,
327 aliases_by_language,
328 preferred_languages: self.preferred_languages,
329 ambiguity_policy: self.ambiguity_policy,
330 })
331 }
332}
333
334#[derive(Clone, Debug, Default)]
336pub struct ReferenceParser {
337 aliases: Vec<CustomAlias>,
338 aliases_by_language: Vec<(Language, CustomAlias)>,
339 preferred_languages: Vec<Language>,
340 ambiguity_policy: AmbiguityPolicy,
341}
342
343impl ReferenceParser {
344 #[must_use]
346 pub fn new() -> Self {
347 Self::default()
348 }
349
350 #[must_use]
352 pub fn builder() -> ParserBuilder {
353 ParserBuilder::default()
354 }
355
356 #[must_use]
358 pub fn preferred_languages(&self) -> &[Language] {
359 &self.preferred_languages
360 }
361
362 pub fn aliases(&self) -> impl Iterator<Item = (&str, Book)> {
364 self.aliases
365 .iter()
366 .map(|alias| (alias.alias.as_ref(), alias.book))
367 }
368
369 pub fn aliases_by_language(&self) -> impl Iterator<Item = (Language, &str, Book)> {
372 self.aliases_by_language
373 .iter()
374 .map(|(language, alias)| (*language, alias.alias.as_ref(), alias.book))
375 }
376
377 #[must_use]
379 pub const fn ambiguity_policy(&self) -> AmbiguityPolicy {
380 self.ambiguity_policy
381 }
382
383 pub fn parse(&self, input: &str) -> Result<Reference, ParseError> {
385 self.parse_detailed(input).map(Parsed::into_value)
386 }
387
388 pub fn parse_with_language(
390 &self,
391 input: &str,
392 language: Language,
393 ) -> Result<Reference, ParseError> {
394 self.parse_detailed_with_language(input, language)
395 .map(Parsed::into_value)
396 }
397
398 pub fn parse_detailed(&self, input: &str) -> Result<Parsed<Reference>, ParseError> {
400 self.parse_detailed_inner(input, Language::Auto)
401 }
402
403 pub fn parse_detailed_with_language(
405 &self,
406 input: &str,
407 language: Language,
408 ) -> Result<Parsed<Reference>, ParseError> {
409 self.parse_detailed_inner(input, language)
410 }
411
412 #[must_use]
414 pub fn try_parse(&self, input: &str) -> Option<Reference> {
415 self.parse(input).ok()
416 }
417
418 #[must_use]
420 pub fn try_parse_with_language(&self, input: &str, language: Language) -> Option<Reference> {
421 self.parse_with_language(input, language).ok()
422 }
423
424 pub fn parse_book(&self, input: &str) -> Result<Book, ParseError> {
426 self.parse_book_detailed(input).map(Parsed::into_value)
427 }
428
429 pub fn parse_book_with_language(
431 &self,
432 input: &str,
433 language: Language,
434 ) -> Result<Book, ParseError> {
435 self.parse_book_detailed_with_language(input, language)
436 .map(Parsed::into_value)
437 }
438
439 pub fn parse_book_detailed(&self, input: &str) -> Result<Parsed<Book>, ParseError> {
441 self.parse_book_detailed_with_language(input, Language::Auto)
442 }
443
444 pub fn parse_book_detailed_with_language(
446 &self,
447 input: &str,
448 language: Language,
449 ) -> Result<Parsed<Book>, ParseError> {
450 let normalized = checked_normalized_input(input)?;
451 let mut matches = Vec::new();
452 let book = self.resolve_book(&normalized, language, &mut matches)?;
453 Ok(Parsed::new(
454 book,
455 ParseMetadata::from_parts(normalized, matches),
456 ))
457 }
458
459 pub fn parse_verse(&self, input: &str) -> Result<VerseRef, ParseError> {
461 self.parse_verse_detailed(input).map(Parsed::into_value)
462 }
463
464 pub fn parse_verse_with_language(
466 &self,
467 input: &str,
468 language: Language,
469 ) -> Result<VerseRef, ParseError> {
470 self.parse_verse_detailed_with_language(input, language)
471 .map(Parsed::into_value)
472 }
473
474 pub fn parse_verse_detailed(&self, input: &str) -> Result<Parsed<VerseRef>, ParseError> {
476 self.parse_verse_detailed_inner(input, Language::Auto)
477 }
478
479 pub fn parse_verse_detailed_with_language(
481 &self,
482 input: &str,
483 language: Language,
484 ) -> Result<Parsed<VerseRef>, ParseError> {
485 self.parse_verse_detailed_inner(input, language)
486 }
487
488 #[must_use]
490 pub fn try_parse_verse(&self, input: &str) -> Option<VerseRef> {
491 self.parse_verse(input).ok()
492 }
493
494 #[must_use]
497 pub fn try_parse_verse_with_language(
498 &self,
499 input: &str,
500 language: Language,
501 ) -> Option<VerseRef> {
502 self.parse_verse_with_language(input, language).ok()
503 }
504
505 pub fn parse_range(&self, input: &str) -> Result<VerseRange, ParseError> {
507 self.parse_range_detailed(input).map(Parsed::into_value)
508 }
509
510 pub fn parse_range_with_language(
512 &self,
513 input: &str,
514 language: Language,
515 ) -> Result<VerseRange, ParseError> {
516 self.parse_range_detailed_with_language(input, language)
517 .map(Parsed::into_value)
518 }
519
520 pub fn parse_range_detailed(&self, input: &str) -> Result<Parsed<VerseRange>, ParseError> {
522 self.parse_range_detailed_inner(input, Language::Auto)
523 }
524
525 pub fn parse_range_detailed_with_language(
527 &self,
528 input: &str,
529 language: Language,
530 ) -> Result<Parsed<VerseRange>, ParseError> {
531 self.parse_range_detailed_inner(input, language)
532 }
533
534 #[must_use]
536 pub fn try_parse_range(&self, input: &str) -> Option<VerseRange> {
537 self.parse_range(input).ok()
538 }
539
540 #[must_use]
543 pub fn try_parse_range_with_language(
544 &self,
545 input: &str,
546 language: Language,
547 ) -> Option<VerseRange> {
548 self.parse_range_with_language(input, language).ok()
549 }
550
551 fn parse_detailed_inner(
552 &self,
553 input: &str,
554 language: Language,
555 ) -> Result<Parsed<Reference>, ParseError> {
556 let normalized = checked_normalized_input(input)?;
557 if find_range_separator(&normalized).is_some() {
558 self.parse_normalized_range(&normalized, language)
559 .map(|parsed| Parsed::new(Reference::Range(parsed.value), parsed.metadata))
560 } else {
561 self.parse_normalized_verse(&normalized, language)
562 .map(|parsed| Parsed::new(Reference::Verse(parsed.value), parsed.metadata))
563 }
564 }
565
566 fn parse_verse_detailed_inner(
567 &self,
568 input: &str,
569 language: Language,
570 ) -> Result<Parsed<VerseRef>, ParseError> {
571 let normalized = checked_normalized_input(input)?;
572 if find_range_separator(&normalized).is_some() {
573 return Err(ParseError::new(
574 ParseErrorKind::PatternMismatch,
575 "expected a single verse, found range syntax",
576 ));
577 }
578 self.parse_normalized_verse(&normalized, language)
579 }
580
581 fn parse_range_detailed_inner(
582 &self,
583 input: &str,
584 language: Language,
585 ) -> Result<Parsed<VerseRange>, ParseError> {
586 let normalized = checked_normalized_input(input)?;
587 if find_range_separator(&normalized).is_none() {
588 return Err(ParseError::new(
589 ParseErrorKind::PatternMismatch,
590 "expected a verse range, found no range separator",
591 ));
592 }
593 self.parse_normalized_range(&normalized, language)
594 }
595
596 fn parse_normalized_verse(
597 &self,
598 normalized: &str,
599 language: Language,
600 ) -> Result<Parsed<VerseRef>, ParseError> {
601 let mut matches = Vec::new();
602 let verse = self.parse_full_endpoint(normalized, language, &mut matches)?;
603 Ok(Parsed::new(
604 verse,
605 ParseMetadata {
606 normalized_input: normalized.to_owned(),
607 book_matches: matches,
608 },
609 ))
610 }
611
612 fn parse_normalized_range(
613 &self,
614 normalized: &str,
615 language: Language,
616 ) -> Result<Parsed<VerseRange>, ParseError> {
617 let separator = find_range_separator(normalized).ok_or_else(|| {
618 ParseError::new(ParseErrorKind::PatternMismatch, "missing range separator")
619 })?;
620 let start_text = &normalized[..separator];
621 let end_text = &normalized[separator + 1..];
622 if find_range_separator(end_text).is_some()
623 || start_text.trim().is_empty()
624 || end_text.trim().is_empty()
625 {
626 return Err(ParseError::new(
627 ParseErrorKind::PatternMismatch,
628 "range must contain exactly two non-empty endpoints",
629 ));
630 }
631
632 let mut matches = Vec::new();
633 let start = self.parse_full_endpoint(start_text, language, &mut matches)?;
634 let end = self.parse_range_end(end_text, start, language, &mut matches)?;
635 let range = VerseRange::new(start, end).map_err(|_| {
636 let kind = if start.book() == end.book() {
637 ParseErrorKind::SameBookRangeNotAscending
638 } else {
639 ParseErrorKind::CrossBookRangeNotAscending
640 };
641 ParseError::new(
642 kind,
643 format!("range end {end} must come after start {start}"),
644 )
645 })?;
646
647 Ok(Parsed::new(
648 range,
649 ParseMetadata {
650 normalized_input: normalized.to_owned(),
651 book_matches: matches,
652 },
653 ))
654 }
655
656 fn parse_full_endpoint(
657 &self,
658 input: &str,
659 language: Language,
660 matches: &mut Vec<BookMatch>,
661 ) -> Result<VerseRef, ParseError> {
662 let (left, verse_token) = split_coordinate(input)?;
663 let (book_token, chapter_token) = split_trailing_number(left, "chapter")?;
664 let book = self.resolve_book(book_token, language, matches)?;
665 let chapter = parse_number(chapter_token, "chapter", MAX_CHAPTER_NUMBER)?;
666 let verse = parse_number(verse_token, "verse", MAX_VERSE_NUMBER)?;
667 VerseRef::new(book, chapter, verse).map_err(|error| {
668 ParseError::new(ParseErrorKind::NumericTokenOutOfRange, error.to_string())
669 })
670 }
671
672 fn parse_range_end(
673 &self,
674 input: &str,
675 start: VerseRef,
676 language: Language,
677 matches: &mut Vec<BookMatch>,
678 ) -> Result<VerseRef, ParseError> {
679 let trimmed = input.trim();
680 if trimmed.bytes().all(|byte| byte.is_ascii_digit()) {
681 let verse = parse_number(trimmed, "end verse", MAX_VERSE_NUMBER)?;
682 return VerseRef::new(start.book(), start.chapter(), verse).map_err(|error| {
683 ParseError::new(ParseErrorKind::NumericTokenOutOfRange, error.to_string())
684 });
685 }
686
687 let (left, verse_token) = split_coordinate(trimmed)?;
688 let (book_token, chapter_token) = split_trailing_number(left, "end chapter")?;
689 let book = if book_token.trim().is_empty() {
690 start.book()
691 } else {
692 self.resolve_book(book_token, language, matches)?
693 };
694 let chapter = parse_number(chapter_token, "end chapter", MAX_CHAPTER_NUMBER)?;
695 let verse = parse_number(verse_token, "end verse", MAX_VERSE_NUMBER)?;
696 VerseRef::new(book, chapter, verse).map_err(|error| {
697 ParseError::new(ParseErrorKind::NumericTokenOutOfRange, error.to_string())
698 })
699 }
700
701 fn resolve_book(
702 &self,
703 token: &str,
704 language: Language,
705 matches: &mut Vec<BookMatch>,
706 ) -> Result<Book, ParseError> {
707 let display_token = token.trim();
708 let key = normalize_book_term(display_token);
709 if key.is_empty() {
710 return Err(ParseError::new(
711 ParseErrorKind::EmptyBookToken,
712 "book token is empty after normalization",
713 ));
714 }
715
716 if !language.is_auto()
717 && !language.is_parsing_supported()
718 && !self
719 .aliases_by_language
720 .iter()
721 .any(|(alias_language, _)| *alias_language == language)
722 {
723 return Err(ParseError::new(
724 ParseErrorKind::UnsupportedLanguage,
725 format!("no aliases are registered for language {}", language.code()),
726 ));
727 }
728
729 let mut candidates = Vec::new();
730 for alias in self.aliases.iter().filter(|alias| alias.normalized == key) {
731 candidates.push(BookCandidate {
732 book: alias.book,
733 alias: Arc::clone(&alias.alias),
734 language: None,
735 custom: true,
736 });
737 }
738
739 if language.is_auto() {
740 self.collect_auto_candidates(&key, &mut candidates);
741 } else {
742 self.collect_language_candidates(language, &key, &mut candidates);
743 }
744
745 let language_priority = self.language_priority();
746 candidates.sort_by_key(|candidate| {
747 let custom_rank = u8::from(!candidate.custom);
748 let language_rank = candidate.language.map_or(0, |language| {
749 language_priority
750 .iter()
751 .position(|candidate| *candidate == language)
752 .map_or(language_priority.len() + 1, |index| index + 1)
753 });
754 (custom_rank, language_rank, candidate.book)
755 });
756 deduplicate_candidates(&mut candidates);
757 let Some(selected) = candidates.first().cloned() else {
758 return Err(ParseError::new(
759 ParseErrorKind::UnknownBook,
760 if language.is_auto() {
761 format!("book token {display_token:?} did not match a known book")
762 } else {
763 format!(
764 "book token {display_token:?} is unknown for language {}",
765 language.code()
766 )
767 },
768 ));
769 };
770
771 let ambiguity_candidates = if candidates.iter().any(|candidate| candidate.custom) {
772 candidates
773 .iter()
774 .filter(|candidate| candidate.custom)
775 .collect::<Vec<_>>()
776 } else {
777 candidates.iter().collect::<Vec<_>>()
778 };
779 let mut distinct_books = Vec::new();
780 for candidate in ambiguity_candidates {
781 if !distinct_books.contains(&candidate.book) {
782 distinct_books.push(candidate.book);
783 }
784 }
785 if self.ambiguity_policy == AmbiguityPolicy::Reject && distinct_books.len() > 1 {
786 let names = distinct_books
787 .iter()
788 .map(|book| book.full_name())
789 .collect::<Vec<_>>()
790 .join(", ");
791 return Err(ParseError::new(
792 ParseErrorKind::AmbiguousBook,
793 format!("book token {display_token:?} matches {names}"),
794 ));
795 }
796
797 matches.push(BookMatch {
798 token: display_token.to_owned(),
799 selected: selected.clone(),
800 alternatives: candidates.into_iter().skip(1).collect(),
801 });
802 Ok(selected.book)
803 }
804
805 fn collect_auto_candidates(&self, key: &str, candidates: &mut Vec<BookCandidate>) {
806 let mut languages = self.language_priority();
807 for language in self
808 .aliases_by_language
809 .iter()
810 .map(|(language, _)| *language)
811 {
812 if !language.is_auto() && !languages.contains(&language) {
813 languages.push(language);
814 }
815 }
816
817 for &language in &languages {
820 for (_, alias) in self
821 .aliases_by_language
822 .iter()
823 .filter(|(alias_language, alias)| {
824 *alias_language == language && alias.normalized == key
825 })
826 {
827 candidates.push(BookCandidate {
828 book: alias.book,
829 alias: Arc::clone(&alias.alias),
830 language: Some(language),
831 custom: true,
832 });
833 }
834 }
835 for &language in &languages {
836 if let Some(records) = bundled_index().get(key) {
837 candidates.extend(
838 records
839 .iter()
840 .filter(|candidate| candidate.language == Some(language))
841 .cloned(),
842 );
843 }
844 }
845 }
846
847 fn language_priority(&self) -> Vec<Language> {
848 let mut languages = Vec::new();
849 for language in self
850 .preferred_languages
851 .iter()
852 .copied()
853 .chain([Language::English])
854 .chain(AUTO_LANGUAGE_PRECEDENCE.iter().copied())
855 .chain(Language::SUPPORTED)
856 {
857 if !language.is_auto() && !languages.contains(&language) {
858 languages.push(language);
859 }
860 }
861 languages
862 }
863
864 fn collect_language_candidates(
865 &self,
866 language: Language,
867 key: &str,
868 candidates: &mut Vec<BookCandidate>,
869 ) {
870 for (_, alias) in self
871 .aliases_by_language
872 .iter()
873 .filter(|(alias_language, alias)| {
874 *alias_language == language && alias.normalized == key
875 })
876 {
877 candidates.push(BookCandidate {
878 book: alias.book,
879 alias: Arc::clone(&alias.alias),
880 language: Some(language),
881 custom: true,
882 });
883 }
884
885 if let Some(records) = bundled_index().get(key) {
886 candidates.extend(
887 records
888 .iter()
889 .filter(|candidate| candidate.language == Some(language))
890 .cloned(),
891 );
892 }
893 }
894}
895
896fn checked_normalized_input(input: &str) -> Result<String, ParseError> {
897 let normalized = normalize_for_parsing(input);
898 if normalized.is_empty() {
899 return Err(ParseError::new(
900 ParseErrorKind::EmptyReference,
901 "reference must not be empty",
902 ));
903 }
904 Ok(normalized)
905}
906
907fn find_range_separator(input: &str) -> Option<usize> {
908 input
909 .match_indices('-')
910 .find_map(|(index, _)| split_coordinate(&input[..index]).is_ok().then_some(index))
911}
912
913fn split_coordinate(input: &str) -> Result<(&str, &str), ParseError> {
914 let separator = input
915 .char_indices()
916 .rev()
917 .find(|(_, character)| matches!(character, ':' | '.'))
918 .map(|(index, character)| (index, character.len_utf8()))
919 .ok_or_else(|| {
920 ParseError::new(
921 ParseErrorKind::MissingNumericToken,
922 "reference is missing a chapter/verse separator",
923 )
924 })?;
925 let left = &input[..separator.0];
926 let right = input[separator.0 + separator.1..].trim();
927 if right.is_empty() {
928 return Err(ParseError::new(
929 ParseErrorKind::MissingNumericToken,
930 "reference is missing a verse number",
931 ));
932 }
933 if !right.bytes().all(|byte| byte.is_ascii_digit()) {
934 return Err(ParseError::new(
935 ParseErrorKind::InvalidNumericToken,
936 format!("verse token {right:?} is not an integer"),
937 ));
938 }
939 Ok((left, right))
940}
941
942fn split_trailing_number<'a>(
943 input: &'a str,
944 component: &str,
945) -> Result<(&'a str, &'a str), ParseError> {
946 let trimmed = input.trim_end();
947 let digit_start = trimmed
948 .char_indices()
949 .rev()
950 .take_while(|(_, character)| character.is_ascii_digit())
951 .last()
952 .map(|(index, _)| index)
953 .ok_or_else(|| {
954 ParseError::new(
955 ParseErrorKind::MissingNumericToken,
956 format!("reference is missing a {component} number"),
957 )
958 })?;
959 Ok((&trimmed[..digit_start], &trimmed[digit_start..]))
960}
961
962fn parse_number(token: &str, component: &str, maximum: u16) -> Result<u16, ParseError> {
963 let value = token.parse::<u32>().map_err(|_| {
964 ParseError::new(
965 ParseErrorKind::InvalidNumericToken,
966 format!("{component} token {token:?} is not an integer"),
967 )
968 })?;
969 if value == 0 {
970 return Err(ParseError::new(
971 ParseErrorKind::NonPositiveNumericToken,
972 format!("{component} must be greater than zero"),
973 ));
974 }
975 if value > u32::from(maximum) {
976 return Err(ParseError::new(
977 ParseErrorKind::NumericTokenOutOfRange,
978 format!("{component} {value} exceeds the sanity limit {maximum}"),
979 ));
980 }
981 Ok(value as u16)
982}
983
984fn normalize_book_term(term: &str) -> String {
985 normalize_syntax(term)
986 .chars()
987 .flat_map(char::to_lowercase)
988 .filter(|character| !character.is_whitespace() && *character != '.')
989 .collect()
990}
991
992fn deduplicate_candidates(candidates: &mut Vec<BookCandidate>) {
993 let mut seen = HashSet::new();
994 candidates
995 .retain(|candidate| seen.insert((candidate.book, candidate.language, candidate.custom)));
996}
997
998#[must_use]
1004pub fn auto_language_collisions() -> &'static BTreeMap<String, Vec<Book>> {
1005 static COLLISIONS: OnceLock<BTreeMap<String, Vec<Book>>> = OnceLock::new();
1006 COLLISIONS.get_or_init(|| {
1007 let mut collisions = BTreeMap::new();
1008 for (alias, candidates) in bundled_index() {
1009 let mut books = candidates
1010 .iter()
1011 .map(BookCandidate::book)
1012 .collect::<Vec<_>>();
1013 books.sort_unstable();
1014 books.dedup();
1015 if books.len() > 1 {
1016 collisions.insert(alias.clone(), books);
1017 }
1018 }
1019 collisions
1020 })
1021}
1022
1023fn bundled_index() -> &'static HashMap<String, Vec<BookCandidate>> {
1024 static INDEX: OnceLock<HashMap<String, Vec<BookCandidate>>> = OnceLock::new();
1025 INDEX.get_or_init(|| {
1026 let mut index: HashMap<String, Vec<BookCandidate>> = HashMap::new();
1027 for &book in Book::ALL {
1028 for alias in [book.full_name(), book.abbreviation()] {
1029 register_bundled_alias(&mut index, alias, book, Language::English);
1030 }
1031 }
1032 for &(alias, book) in COMMON_ENGLISH_ALIASES {
1033 register_bundled_alias(&mut index, alias, book, Language::English);
1034 }
1035 for &language in AUTO_LANGUAGE_PRECEDENCE {
1036 if let Some(books) = localized_aliases(language) {
1037 for record in books {
1038 for alias in record.all_aliases() {
1039 register_bundled_alias(&mut index, alias, record.book, language);
1040 }
1041 }
1042 }
1043 }
1044 index
1045 })
1046}
1047
1048fn register_bundled_alias(
1049 index: &mut HashMap<String, Vec<BookCandidate>>,
1050 alias: &str,
1051 book: Book,
1052 language: Language,
1053) {
1054 index
1055 .entry(normalize_book_term(alias))
1056 .or_default()
1057 .push(BookCandidate {
1058 book,
1059 alias: Arc::from(alias),
1060 language: Some(language),
1061 custom: false,
1062 });
1063}
1064
1065const COMMON_ENGLISH_ALIASES: &[(&str, Book)] = &[
1069 ("Gen", Book::Genesis),
1070 ("Exod", Book::Exodus),
1071 ("Lev", Book::Leviticus),
1072 ("Num", Book::Numbers),
1073 ("Deut", Book::Deuteronomy),
1074 ("Josh", Book::Joshua),
1075 ("Judg", Book::Judges),
1076 ("1 Sam", Book::FirstSamuel),
1077 ("2 Sam", Book::SecondSamuel),
1078 ("1 Kings", Book::FirstKings),
1079 ("2 Kings", Book::SecondKings),
1080 ("1 Chr", Book::FirstChronicles),
1081 ("2 Chr", Book::SecondChronicles),
1082 ("Neh", Book::Nehemiah),
1083 ("Est", Book::Esther),
1084 ("Prov", Book::Proverbs),
1085 ("Eccl", Book::Ecclesiastes),
1086 ("Song", Book::SongOfSolomon),
1087 ("Isa", Book::Isaiah),
1088 ("Jer", Book::Jeremiah),
1089 ("Lam", Book::Lamentations),
1090 ("Ezek", Book::Ezekiel),
1091 ("Dan", Book::Daniel),
1092 ("Hos", Book::Hosea),
1093 ("Obad", Book::Obadiah),
1094 ("Mic", Book::Micah),
1095 ("Nah", Book::Nahum),
1096 ("Hab", Book::Habakkuk),
1097 ("Zeph", Book::Zephaniah),
1098 ("Zech", Book::Zechariah),
1099 ("Matt", Book::Matthew),
1100 ("Mar", Book::Mark),
1101 ("Luk", Book::Luke),
1102 ("Joh", Book::John),
1103 ("Rom", Book::Romans),
1104 ("1 Cor", Book::FirstCorinthians),
1105 ("2 Cor", Book::SecondCorinthians),
1106 ("Gal", Book::Galatians),
1107 ("Phil", Book::Philippians),
1108 ("Col", Book::Colossians),
1109 ("1 Thess", Book::FirstThessalonians),
1110 ("2 Thess", Book::SecondThessalonians),
1111 ("1 Tim", Book::FirstTimothy),
1112 ("2 Tim", Book::SecondTimothy),
1113 ("Phlm", Book::Philemon),
1114 ("Heb", Book::Hebrews),
1115 ("Jas", Book::James),
1116 ("1 Pet", Book::FirstPeter),
1117 ("2 Pet", Book::SecondPeter),
1118 ("1 John", Book::FirstJohn),
1119 ("2 John", Book::SecondJohn),
1120 ("3 John", Book::ThirdJohn),
1121 ("Rev", Book::Revelation),
1122];
1123
1124#[cfg(test)]
1125#[path = "../tests/unit/parser.rs"]
1126mod tests;