From 05b712d084ebd99877ace410f0041f28baac7e42 Mon Sep 17 00:00:00 2001 From: funkpower Date: Mon, 21 Sep 2026 11:30:26 -0400 Subject: [PATCH] =?UTF-8?q?0.2.2=20=E2=80=94=20a=20subtitle=20begins=20wit?= =?UTF-8?q?h=20a=20capital?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit title_case counted small words from the start of the whole string, so every subtitle that opened with one was set in lower case: 'Aesthetic in Kant: a Critique', 'Born Fighting: the Scots-Irish'. A colon starts a title of its own and the first word of one is capitalised however small it is. Found by book-pipeline, which had been proposing a title page's capitals verbatim into a catalogue and needed this to stop. --- Cargo.lock | 52 ++++++++++++++++++------------------ Cargo.toml | 54 +++++++++++++++++++------------------- crates/bibclean/src/fix.rs | 46 +++++++++++++++++++++++++++++++- crates/bibclean/src/lib.rs | 7 ++++- 4 files changed, 104 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 14d0a71..76e5886 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,7 +101,7 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "avplan" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -126,7 +126,7 @@ checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "bibclean" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -143,7 +143,7 @@ dependencies = [ [[package]] name = "bibfetch" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -196,7 +196,7 @@ dependencies = [ [[package]] name = "bookmeta" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -210,7 +210,7 @@ dependencies = [ [[package]] name = "booksniff" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -250,7 +250,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "catena" -version = "0.2.1" +version = "0.2.2" dependencies = [ "bibclean", "bibfetch", @@ -277,7 +277,7 @@ dependencies = [ [[package]] name = "catena-cli" -version = "0.2.1" +version = "0.2.2" dependencies = [ "clap", "clap_mangen", @@ -312,7 +312,7 @@ checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "chunkmd" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -334,7 +334,7 @@ dependencies = [ [[package]] name = "citefind" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "chunkmd", @@ -399,7 +399,7 @@ dependencies = [ [[package]] name = "colophon" -version = "0.2.1" +version = "0.2.2" dependencies = [ "bibclean", "catena-cli", @@ -448,7 +448,7 @@ dependencies = [ [[package]] name = "corpus-core" -version = "0.2.1" +version = "0.2.2" dependencies = [ "schemars", "serde", @@ -492,7 +492,7 @@ dependencies = [ [[package]] name = "cuekit" -version = "0.2.1" +version = "0.2.2" dependencies = [ "corpus-core", "regex", @@ -504,7 +504,7 @@ dependencies = [ [[package]] name = "cuenorm" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -514,7 +514,7 @@ dependencies = [ [[package]] name = "cuepack" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -525,7 +525,7 @@ dependencies = [ [[package]] name = "cueseg" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -612,7 +612,7 @@ dependencies = [ [[package]] name = "emb" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -876,7 +876,7 @@ dependencies = [ [[package]] name = "idnorm" -version = "0.2.1" +version = "0.2.2" dependencies = [ "bibclean", "catena-cli", @@ -942,7 +942,7 @@ dependencies = [ [[package]] name = "langid" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -1022,7 +1022,7 @@ dependencies = [ [[package]] name = "mdscrub" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -1034,7 +1034,7 @@ dependencies = [ [[package]] name = "mdsplit" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -1113,7 +1113,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "pdfgutter" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -1126,7 +1126,7 @@ dependencies = [ [[package]] name = "pdftriage" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -1370,7 +1370,7 @@ checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189" [[package]] name = "rrf" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -1517,7 +1517,7 @@ dependencies = [ [[package]] name = "seriesof" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", @@ -1634,7 +1634,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "textkit" -version = "0.2.1" +version = "0.2.2" dependencies = [ "regex", "tiktoken-rs", @@ -1724,7 +1724,7 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tocfind" -version = "0.2.1" +version = "0.2.2" dependencies = [ "catena-cli", "clap", diff --git a/Cargo.toml b/Cargo.toml index dc077a4..5d40ac7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,39 +3,39 @@ resolver = "3" members = ["crates/*"] [workspace.package] -version = "0.2.1" +version = "0.2.2" edition = "2024" rust-version = "1.85" license = "MPL-2.0" repository = "https://git.pumpin.online/funkpower/catena" [workspace.dependencies] -catena-cli = { path = "crates/catena-cli", version = "0.2.1" } -corpus-core = { path = "crates/corpus-core", version = "0.2.1" } -cuekit = { path = "crates/cuekit", version = "0.2.1" } -textkit = { path = "crates/textkit", version = "0.2.1" } -chunkmd = { path = "crates/chunkmd", version = "0.2.1" } -mdscrub = { path = "crates/mdscrub", version = "0.2.1" } -mdsplit = { path = "crates/mdsplit", version = "0.2.1" } -pdftriage = { path = "crates/pdftriage", version = "0.2.1" } -cuenorm = { path = "crates/cuenorm", version = "0.2.1" } -cueseg = { path = "crates/cueseg", version = "0.2.1" } -rrf = { path = "crates/rrf", version = "0.2.1" } -cuepack = { path = "crates/cuepack", version = "0.2.1" } -bibclean = { path = "crates/bibclean", version = "0.2.1" } -avplan = { path = "crates/avplan", version = "0.2.1" } -pdfgutter = { path = "crates/pdfgutter", version = "0.2.1" } -catena = { path = "crates/bindings", version = "0.2.1" } -seriesof = { path = "crates/seriesof", version = "0.2.1" } -idnorm = { path = "crates/idnorm", version = "0.2.1" } -bibfetch = { path = "crates/bibfetch", version = "0.2.1" } -bookmeta = { path = "crates/bookmeta", version = "0.2.1" } -langid = { path = "crates/langid", version = "0.2.1" } -tocfind = { path = "crates/tocfind", version = "0.2.1" } -booksniff = { path = "crates/booksniff", version = "0.2.1" } -citefind = { path = "crates/citefind", version = "0.2.1" } -colophon = { path = "crates/colophon", version = "0.2.1" } -emb = { path = "crates/emb", version = "0.2.1" } +catena-cli = { path = "crates/catena-cli", version = "0.2.2" } +corpus-core = { path = "crates/corpus-core", version = "0.2.2" } +cuekit = { path = "crates/cuekit", version = "0.2.2" } +textkit = { path = "crates/textkit", version = "0.2.2" } +chunkmd = { path = "crates/chunkmd", version = "0.2.2" } +mdscrub = { path = "crates/mdscrub", version = "0.2.2" } +mdsplit = { path = "crates/mdsplit", version = "0.2.2" } +pdftriage = { path = "crates/pdftriage", version = "0.2.2" } +cuenorm = { path = "crates/cuenorm", version = "0.2.2" } +cueseg = { path = "crates/cueseg", version = "0.2.2" } +rrf = { path = "crates/rrf", version = "0.2.2" } +cuepack = { path = "crates/cuepack", version = "0.2.2" } +bibclean = { path = "crates/bibclean", version = "0.2.2" } +avplan = { path = "crates/avplan", version = "0.2.2" } +pdfgutter = { path = "crates/pdfgutter", version = "0.2.2" } +catena = { path = "crates/bindings", version = "0.2.2" } +seriesof = { path = "crates/seriesof", version = "0.2.2" } +idnorm = { path = "crates/idnorm", version = "0.2.2" } +bibfetch = { path = "crates/bibfetch", version = "0.2.2" } +bookmeta = { path = "crates/bookmeta", version = "0.2.2" } +langid = { path = "crates/langid", version = "0.2.2" } +tocfind = { path = "crates/tocfind", version = "0.2.2" } +booksniff = { path = "crates/booksniff", version = "0.2.2" } +citefind = { path = "crates/citefind", version = "0.2.2" } +colophon = { path = "crates/colophon", version = "0.2.2" } +emb = { path = "crates/emb", version = "0.2.2" } clap = { version = "4", features = ["derive"] } clap_mangen = "0.3" diff --git a/crates/bibclean/src/fix.rs b/crates/bibclean/src/fix.rs index 28a1f09..a1a201d 100644 --- a/crates/bibclean/src/fix.rs +++ b/crates/bibclean/src/fix.rs @@ -67,7 +67,19 @@ pub fn title_case(title: &str) -> String { .to_lowercase() .trim_end_matches(['.', ',', ':', ';', '!', '?', ')', ']', '"', '\'']) .to_string(); - if SMALL_WORDS.binary_search(&core.as_str()).is_ok() && at > 0 && at < last { + // A small word stays small in the middle of a title, but not + // where a title actually starts. A colon starts a subtitle, and + // "Aesthetic in Kant: a Critique" is the giveaway that the rule + // was only ever counting from the beginning of the whole string. + let opens_a_subtitle = at > 0 + && words[at - 1] + .trim_end_matches(['"', '\'', ')', ']']) + .ends_with([':', ';', '?', '!', '.']); + if SMALL_WORDS.binary_search(&core.as_str()).is_ok() + && at > 0 + && at < last + && !opens_a_subtitle + { return word.to_lowercase(); } let lowered = word.to_lowercase(); @@ -218,3 +230,35 @@ mod tests { ); } } + +#[cfg(test)] +mod subtitle_case_tests { + use super::title_case; + + #[test] + fn a_subtitle_starts_with_a_capital() { + // The rule counted small words from the start of the string, so every + // subtitle beginning "A", "The" or "Of" was set lower case. + assert_eq!( + title_case("AESTHETIC IN KANT: A CRITIQUE"), + "Aesthetic in Kant: A Critique" + ); + assert_eq!( + title_case("BORN FIGHTING: THE SCOTS-IRISH"), + "Born Fighting: The Scots-Irish" + ); + assert_eq!( + title_case("A HISTORY: AN INTRODUCTION TO IT"), + "A History: An Introduction to It" + ); + } + + #[test] + fn a_small_word_in_the_middle_is_still_small() { + assert_eq!( + title_case("THE RISE AND FALL OF THE THIRD REICH"), + "The Rise and Fall of the Third Reich" + ); + assert_eq!(title_case("WAR OF THE WORLDS"), "War of the Worlds"); + } +} diff --git a/crates/bibclean/src/lib.rs b/crates/bibclean/src/lib.rs index 93b001a..0592425 100644 --- a/crates/bibclean/src/lib.rs +++ b/crates/bibclean/src/lib.rs @@ -260,9 +260,14 @@ mod tests { #[test] fn an_underscored_title_is_cleaned_before_it_is_cased() { let cleaned = clean(&book("THE FALL_ AND THE RISE_txt", "Arendt, Hannah")); + // "And" is capitalised because the colon starts a subtitle, and the + // first word of one is capitalised however small it is. This asserted + // "and" until the rule was found to be counting small words from the + // start of the whole string, which set every subtitle opening "A", + // "The" or "An" in lower case. assert_eq!( cleaned.book.title.as_deref(), - Some("The Fall: and the Rise") + Some("The Fall: And the Rise") ); assert_eq!(cleaned.changes.len(), 2); }