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.
This commit is contained in:
funkpower
2026-09-21 15:47:24 -04:00
parent 05b712d084
commit 90fb977a10
3 changed files with 149 additions and 53 deletions
Generated
+26 -26
View File
@@ -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",
+27 -27
View File
@@ -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"
+96
View File
@@ -35,6 +35,29 @@ static LIFESPAN: LazyLock<Regex> = LazyLock::new(|| {
static BARE_DATE: LazyLock<Regex> =
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<Regex> =
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<Regex> =
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<Regex> = 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<Regex> = 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<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)^\s*(to|for|in memory of)\s+\S.*,\s*$").expect("a valid pattern")
});
static COPYRIGHT_NOTICE: LazyLock<Regex> = 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:?}"
);
}
}
}