colophon: file a book's printed subjects onto a shelf someone has

A cataloguing block states subjects in Library of Congress form, which
is precise, deep and no use for browsing. Real blocks from this library
read 'Political science – Early works to 1800' and 'English
literature—Middle English, 1100–1500—History and criticism'. Nobody has
a shelf called that.

--shelf takes the closed list the library is actually browsed by and
files the printed headings onto it, longest term first so a period of
thought does not collapse into its parent. Matching is on whole words
after folding punctuation away, because a cataloguing block separates
with em dashes, en dashes, full stops and plain hyphens, often three in
one heading, and OCR turns any of those into any other.

What matches nothing comes back under unshelved rather than being
dropped: a subject silently discarded is indistinguishable from a book
that printed none, and the difference is the whole value of reading the
block.
This commit is contained in:
funkpower
2026-09-21 10:14:52 -04:00
parent 4ea28c03b6
commit 6199fce251
4 changed files with 359 additions and 2 deletions
+31 -1
View File
@@ -53,6 +53,16 @@ struct Args {
/// How many pages in from the front to read.
#[arg(long, value_name = "COUNT", default_value_t = colophon::PAGES)]
pages: usize,
/// A closed list of headings to file the book's printed subjects under.
///
/// One heading per line, optionally `Heading = term, term` to give the
/// words that should match it; a heading with no terms matches its own
/// name. A cataloguing block states subjects in Library of Congress form,
/// which is no use as a shelf, so this says what the shelf is. What
/// matches nothing comes back under `unshelved` rather than being dropped.
#[arg(long, value_name = "FILE")]
shelf: Option<PathBuf>,
}
fn main() -> ExitCode {
@@ -73,6 +83,26 @@ fn main() -> ExitCode {
return ExitCode::from(exit::USAGE);
}
// Read the shelf before any book, so a bad path fails at once rather than
// after the whole library has been read with no shelf applied.
let shelf = match args.shelf.as_deref() {
None => None,
Some(path) => match std::fs::read_to_string(path) {
Ok(text) => {
let s = colophon::shelf::Shelf::parse(&text);
if s.is_empty() {
eprintln!("colophon: {} lists no headings", path.display());
return ExitCode::from(exit::USAGE);
}
Some(s)
}
Err(e) => {
eprintln!("colophon: {}: {e}", path.display());
return ExitCode::from(exit::USAGE);
}
},
};
let inputs: Vec<Option<PathBuf>> = if args.paths.is_empty() {
vec![None]
} else {
@@ -89,7 +119,7 @@ fn main() -> ExitCode {
return ExitCode::from(code);
}
};
let mut record = colophon::read(&text, args.pages);
let mut record = colophon::read_onto(&text, args.pages, shelf.as_ref());
record.path = path.as_deref().map(|p| p.display().to_string());
records.push(record);
last_text = text;