diff --git a/crates/citefind/README.md b/crates/citefind/README.md index c3ba874..db55c09 100644 --- a/crates/citefind/README.md +++ b/crates/citefind/README.md @@ -181,6 +181,11 @@ back. Nor does a run carrying a page reference or a semicolon count as one: that is two entries run together, or a reference to a work rather than the name of one. +**Take chapter and verse for a work.** `Matthew 4:17` and `Cf. Deuteronomy +18:15-19; Isaiah 53` are scripture references, which is most of what the notes +of a theological book are. Nobody is looking for the publisher of Galatians, +so they are not references and no record is written for them. + **Name a place of publication as the publisher.** `(London, 1973)` says where a book was published and not by whom, so `container` stays empty. The place is still in `text`. diff --git a/crates/citefind/src/parse.rs b/crates/citefind/src/parse.rs index f124075..744077b 100644 --- a/crates/citefind/src/parse.rs +++ b/crates/citefind/src/parse.rs @@ -128,6 +128,18 @@ static NOT_A_TITLE: LazyLock = LazyLock::new(|| { .expect("a valid pattern") }); +/// A reference to chapter and verse: `Matthew 4:17`, `1 Peter 2:6-8`. +/// +/// A book of the Bible is not a work in a bibliography — nobody is looking +/// for the publisher of Galatians — and a page of them is what the notes of +/// a theological book largely are. +static VERSE: LazyLock = LazyLock::new(|| { + Regex::new( + r"(?i)^(?:cf\.?|see|and)?\s*(?:[1-4]\s+)?\p{L}[\p{L}.]{2,14}\.?\s+\d{1,3}(?::\d{1,3}(?:[-–]\d{1,3})?)?\.?$", + ) + .expect("a valid pattern") +}); + /// A page reference or a bracketed imprint: what a citation has and a remark /// about the argument does not. static CITATION_ANCHOR: LazyLock = LazyLock::new(|| { @@ -216,6 +228,9 @@ pub fn one(text: &str, marker: Option<&str>) -> Option { if !CITATION_SIGNAL.is_match(text) { return None; } + if is_scripture(text) { + return None; + } // A note that runs on for six sentences with no imprint, no page // reference and no identifier is a remark about the argument. The LLM // path this replaces was told to skip those; a parser has to decide. @@ -436,6 +451,20 @@ fn strip_markup(text: &str) -> String { without_tags.replace(['*', '_'], "").trim().to_string() } +/// Whether an entry is chapter and verse and nothing else. +/// +/// Several of them separated by semicolons is still a scripture reference +/// and still not a work: `Cf. Deuteronomy 18:15-19; Isaiah 53; Acts 1:6`. +fn is_scripture(text: &str) -> bool { + let text = text.trim_start_matches(['.', ' ']); + let parts: Vec<&str> = text + .split(';') + .map(str::trim) + .filter(|part| !part.is_empty()) + .collect(); + !parts.is_empty() && parts.iter().all(|part| VERSE.is_match(part)) +} + /// Every identifier the entry carries, normalised. /// /// Order is fixed rather than found: two entries carrying the same @@ -1075,6 +1104,26 @@ mod tests { assert_eq!(found.container, None); } + #[test] + fn chapter_and_verse_is_not_a_work_in_a_bibliography() { + for verse in [ + "Matthew 4:17", + ". Cf. Deuteronomy 18:15-19; Isaiah 53; Acts 1:6", + "1 Peter 2:6-8", + "Matthew 13:43; cf Exodus 34:29-35", + ] { + assert!(one(verse, None).is_none(), "{verse}"); + } + // A journal and its volume look similar and are not the same thing. + assert!( + one( + "Altmann, M. 1951, A Study of Behaviour. Sociometry 14, 351-354.", + None + ) + .is_some() + ); + } + #[test] fn a_heading_or_a_page_number_is_not_a_reference() { for junk in ["Bibliography", "233", "Notes to pages 12-14", "see below"] { diff --git a/crates/citefind/src/split.rs b/crates/citefind/src/split.rs index 60d1594..72b952f 100644 --- a/crates/citefind/src/split.rs +++ b/crates/citefind/src/split.rs @@ -249,7 +249,11 @@ fn strip_marker(raw: &str) -> Entry { let consumed = bare[..whole.end()].chars().count(); let cut = from[consumed]; return Entry { - text: text[cut..].trim_start().to_string(), + // `[4] . Smith, John` leaves the stop that belonged to the + // marker sitting in front of the entry. + text: text[cut..] + .trim_start_matches(|c: char| ".) ".contains(c)) + .to_string(), marker: Some(found[1].to_string()), }; } @@ -425,6 +429,13 @@ Another Anonymous Work. Cambridge, 2001."; assert_eq!(entry.text, "Smith, *Title*, p. 4."); } + #[test] + fn a_stop_that_belonged_to_the_marker_does_not_start_the_entry() { + let entry = strip_marker("[4] . Smith, John. *A Real Book*. Oxford, 1998."); + assert_eq!(entry.marker.as_deref(), Some("4")); + assert_eq!(entry.text, "Smith, John. *A Real Book*. Oxford, 1998."); + } + #[test] fn an_entry_with_no_marker_keeps_all_of_itself() { let entry = strip_marker("Smith, John. The Title. Oxford, 1998.");