From 90fb977a10c9e12c092f8eb5dc14254ef598d694 Mon Sep 17 00:00:00 2001 From: funkpower Date: Mon, 21 Sep 2026 15:47:24 -0400 Subject: [PATCH] colophon: six more things the front matter of a book is not Read out of the review queue a real run produced, where each of these was sitting as a proposed title waiting for a person to say no: 1. General Remarks a numbered heading 2 James W. McAllister a numbered heading 5222 What Is This a folio that ran into the line 827 North Fourth Street an address Page xxxvi / Chapter I a marker title a form label, printed literally Sidebottom, Harry. Ancient a bibliography entry To Mackenzie, a dedication The numbers are the delicate part. A leading number that could be a year is left alone, because 1984 Revisited is a book and 5222 What Is This is a folio, and nothing else distinguishes them. A dedication has to end in a comma, so To Kill a Mockingbird survives. The copyright notices in the same pile are already fixed: those proposals were written before 0.2.2 and are stale rather than wrong. --- Cargo.lock | 52 +++++++++---------- Cargo.toml | 54 ++++++++++---------- crates/colophon/src/title.rs | 96 ++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76e5886..c22c3de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,7 +101,7 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "avplan" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -126,7 +126,7 @@ checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "bibclean" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -143,7 +143,7 @@ dependencies = [ [[package]] name = "bibfetch" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -196,7 +196,7 @@ dependencies = [ [[package]] name = "bookmeta" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -210,7 +210,7 @@ dependencies = [ [[package]] name = "booksniff" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -250,7 +250,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "catena" -version = "0.2.2" +version = "0.2.3" dependencies = [ "bibclean", "bibfetch", @@ -277,7 +277,7 @@ dependencies = [ [[package]] name = "catena-cli" -version = "0.2.2" +version = "0.2.3" dependencies = [ "clap", "clap_mangen", @@ -312,7 +312,7 @@ checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "chunkmd" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -334,7 +334,7 @@ dependencies = [ [[package]] name = "citefind" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "chunkmd", @@ -399,7 +399,7 @@ dependencies = [ [[package]] name = "colophon" -version = "0.2.2" +version = "0.2.3" dependencies = [ "bibclean", "catena-cli", @@ -448,7 +448,7 @@ dependencies = [ [[package]] name = "corpus-core" -version = "0.2.2" +version = "0.2.3" dependencies = [ "schemars", "serde", @@ -492,7 +492,7 @@ dependencies = [ [[package]] name = "cuekit" -version = "0.2.2" +version = "0.2.3" dependencies = [ "corpus-core", "regex", @@ -504,7 +504,7 @@ dependencies = [ [[package]] name = "cuenorm" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -514,7 +514,7 @@ dependencies = [ [[package]] name = "cuepack" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -525,7 +525,7 @@ dependencies = [ [[package]] name = "cueseg" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -612,7 +612,7 @@ dependencies = [ [[package]] name = "emb" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -876,7 +876,7 @@ dependencies = [ [[package]] name = "idnorm" -version = "0.2.2" +version = "0.2.3" dependencies = [ "bibclean", "catena-cli", @@ -942,7 +942,7 @@ dependencies = [ [[package]] name = "langid" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -1022,7 +1022,7 @@ dependencies = [ [[package]] name = "mdscrub" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -1034,7 +1034,7 @@ dependencies = [ [[package]] name = "mdsplit" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -1113,7 +1113,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "pdfgutter" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -1126,7 +1126,7 @@ dependencies = [ [[package]] name = "pdftriage" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -1370,7 +1370,7 @@ checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189" [[package]] name = "rrf" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -1517,7 +1517,7 @@ dependencies = [ [[package]] name = "seriesof" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", @@ -1634,7 +1634,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "textkit" -version = "0.2.2" +version = "0.2.3" dependencies = [ "regex", "tiktoken-rs", @@ -1724,7 +1724,7 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tocfind" -version = "0.2.2" +version = "0.2.3" dependencies = [ "catena-cli", "clap", diff --git a/Cargo.toml b/Cargo.toml index 5d40ac7..94f11b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,39 +3,39 @@ resolver = "3" members = ["crates/*"] [workspace.package] -version = "0.2.2" +version = "0.2.3" 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.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" } +catena-cli = { path = "crates/catena-cli", version = "0.2.3" } +corpus-core = { path = "crates/corpus-core", version = "0.2.3" } +cuekit = { path = "crates/cuekit", version = "0.2.3" } +textkit = { path = "crates/textkit", version = "0.2.3" } +chunkmd = { path = "crates/chunkmd", version = "0.2.3" } +mdscrub = { path = "crates/mdscrub", version = "0.2.3" } +mdsplit = { path = "crates/mdsplit", version = "0.2.3" } +pdftriage = { path = "crates/pdftriage", version = "0.2.3" } +cuenorm = { path = "crates/cuenorm", version = "0.2.3" } +cueseg = { path = "crates/cueseg", version = "0.2.3" } +rrf = { path = "crates/rrf", version = "0.2.3" } +cuepack = { path = "crates/cuepack", version = "0.2.3" } +bibclean = { path = "crates/bibclean", version = "0.2.3" } +avplan = { path = "crates/avplan", version = "0.2.3" } +pdfgutter = { path = "crates/pdfgutter", version = "0.2.3" } +catena = { path = "crates/bindings", version = "0.2.3" } +seriesof = { path = "crates/seriesof", version = "0.2.3" } +idnorm = { path = "crates/idnorm", version = "0.2.3" } +bibfetch = { path = "crates/bibfetch", version = "0.2.3" } +bookmeta = { path = "crates/bookmeta", version = "0.2.3" } +langid = { path = "crates/langid", version = "0.2.3" } +tocfind = { path = "crates/tocfind", version = "0.2.3" } +booksniff = { path = "crates/booksniff", version = "0.2.3" } +citefind = { path = "crates/citefind", version = "0.2.3" } +colophon = { path = "crates/colophon", version = "0.2.3" } +emb = { path = "crates/emb", version = "0.2.3" } clap = { version = "4", features = ["derive"] } clap_mangen = "0.3" diff --git a/crates/colophon/src/title.rs b/crates/colophon/src/title.rs index 4853573..8f6d1c7 100644 --- a/crates/colophon/src/title.rs +++ b/crates/colophon/src/title.rs @@ -35,6 +35,29 @@ static LIFESPAN: LazyLock = LazyLock::new(|| { static BARE_DATE: LazyLock = LazyLock::new(|| Regex::new(r"(?i)^\d{1,4}\s*(bc|ad|bce|ce)?\.?$").expect("a valid pattern")); /// `Copyright © 2013 by`, `\u{a9} 2013 Richard Swinburne`, `Copyright 1971 by`. +/// `1. General Remarks`, `2 James W. McAllister` — a numbered heading. +static NUMBERED_HEADING: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*\d+\s*[.)]\s+\S|^\s*\d+\s{2,}\S").expect("a valid pattern")); +/// `5222 What Is This`, `827 North Fourth Street` — a number that is not a +/// year, leading a line. +static LEADING_NUMBER: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*(\d{1,5})\s+\S").expect("a valid pattern")); +/// `Page xxxvi`, `Chapter I`, `Fig. 3` — a marker, not a name. +static MARKER: LazyLock = LazyLock::new(|| { + Regex::new( + r"(?i)^\s*(page|pages|p\.|pp\.|fig\.|figure|plate|chapter|ch\.|section|part)\s+[\divxlcm]", + ) + .expect("a valid pattern") +}); +/// `Sidebottom, Harry. Ancient warfare` — a bibliography entry, which front +/// matter prints in its cataloguing block and in its further reading. +static CATALOGUE_ENTRY: LazyLock = LazyLock::new(|| { + Regex::new(r"^[A-Z][A-Za-z'\-]+,\s+[A-Z][A-Za-z'.\-]*\.\s+\S").expect("a valid pattern") +}); +/// `To Mackenzie,` — a dedication, set alone on its own page. +static DEDICATION: LazyLock = LazyLock::new(|| { + Regex::new(r"(?i)^\s*(to|for|in memory of)\s+\S.*,\s*$").expect("a valid pattern") +}); static COPYRIGHT_NOTICE: LazyLock = LazyLock::new(|| { // The year need not follow the symbol directly: "(c) Alfredo Ferrarin // 2004" puts the holder between them, and was read as a title. @@ -79,10 +102,13 @@ copenhagen|stockholm|moscow|beijing|tokyo|delhi|nairobi)\b[\s,·•|and]*", static STRUCTURAL: &[&str] = &[ "abbreviations", "about the author", + "abstract", "acknowledgements", "acknowledgments", "afterword", "appendix", + "author", + "authors", "bibliography", "contents", "dedication", @@ -95,6 +121,7 @@ static STRUCTURAL: &[&str] = &[ "illustrations", "index", "introduction", + "keywords", "list of contributors", "list of illustrations", "maps", @@ -102,7 +129,9 @@ static STRUCTURAL: &[&str] = &[ "postscript", "preface", "prologue", + "subtitle", "table of contents", + "title", "title page", "translator's note", ]; @@ -182,6 +211,22 @@ fn is_furniture(line: &str) -> bool { } // Sentences a copyright page sets, which are not notices and so not caught // by the notice pattern. + if NUMBERED_HEADING.is_match(t) || MARKER.is_match(t) || CATALOGUE_ENTRY.is_match(t) { + return true; + } + // A line opening with a number that is not a year. "5222 What Is This" and + // "827 North Fourth Street" are a stray folio and an address; "1984 + // Revisited" is a book, and the difference is whether the number could be + // the year of one. + if let Some(c) = LEADING_NUMBER.captures(t) { + let n: u32 = c[1].parse().unwrap_or(0); + if !(1000..=2099).contains(&n) { + return true; + } + } + if DEDICATION.is_match(t) { + return true; + } let lower = t.to_lowercase(); for phrase in [ "moral rights of the author", @@ -496,3 +541,54 @@ mod conversion_tests { } } } + +#[cfg(test)] +mod review_pile_tests { + use super::*; + + /// Everything here was proposed as a title by a real run over this + /// library, and sat in the review queue waiting for a person to say no. + #[test] + fn what_the_review_pile_was_full_of() { + for line in [ + "1. General Remarks", + "2 James W. McAllister", + "5222 What Is This", + "827 North Fourth Street", + "Page xxxvi", + "Page xxxviii", + "Chapter I", + "title", + "Sidebottom, Harry. Ancient warfare", + "To Mackenzie,", + ] { + assert!( + is_structural(line) || is_furniture(line), + "still read as a title: {line:?}" + ); + } + } + + /// And none of it catches a book. The numbers are the delicate part: a + /// year can open a title, and a folio cannot. + #[test] + fn a_book_is_still_a_book() { + for line in [ + "1984 Revisited", + "1914: The Year the World Ended", + "Chapter and Verse", + "Part of the Furniture", + "To Kill a Mockingbird", + "For Whom the Bell Tolls", + "The Title of This Book", + "Pages from a Cold Island", + "Sections of a Broken Whole", + "In Memory of Bread", + ] { + assert!( + !(is_structural(line) || is_furniture(line)), + "rejected a real title: {line:?}" + ); + } + } +}