From f4497b3f0abe236f37f21df43de9ec73b20316d7 Mon Sep 17 00:00:00 2001 From: funkpower Date: Mon, 21 Sep 2026 10:47:10 -0400 Subject: [PATCH] colophon: the front matter of a book nobody typeset carefully Read against five thousand real books, the title page is often not the first thing in the file, and what is there instead has shape. Six kinds of it were being read as titles: vi CONTENTS a folioed heading Copyright (c) 2013 by Barry Davies, BEM a notice Translated from the French by Willard R. Trask a credit Vol. 11. HENRY JAMES, JR. a collected edition's list [PART ONE: a part heading Piper Verlag GmbH, Munich = LazyLock::new(|| { /// `350 BC`, `1971` — a date alone on a line, which is not a title. 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`. +static COPYRIGHT_NOTICE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?i)(\u{a9}|\(c\)|copyright)\s*(\u{a9}|\(c\))?\s*(1[0-9]|20)\d{2}") + .expect("a valid pattern") +}); /// `Edited by`, with whoever it was set on the line beneath. static ROLE_ONLY: LazyLock = LazyLock::new(|| { Regex::new(r"(?i)^(edited|translated|written|selected|compiled)\s+by:?$") @@ -107,7 +112,111 @@ fn is_structural(line: &str) -> bool { .collect::() .trim() .to_lowercase(); - STRUCTURAL.binary_search(&norm.as_str()).is_ok() + let norm = strip_folio(&norm); + STRUCTURAL.binary_search(&norm).is_ok() +} + +/// A heading with its page number taken off either end. +/// +/// Front matter is folioed in roman, and the folio sits on the same line as +/// the heading often enough to matter: `vi CONTENTS` and `viii CONTENTS` +/// are contents pages, and were being read as titles because they are not the +/// bare word. +fn strip_folio(norm: &str) -> &str { + let is_folio = |w: &str| { + !w.is_empty() + && (w.chars().all(|c| c.is_ascii_digit()) + || w.chars() + .all(|c| matches!(c, 'i' | 'v' | 'x' | 'l' | 'c' | 'd' | 'm'))) + }; + let mut out = norm.trim(); + loop { + let trimmed = match out.split_once(' ') { + Some((head, rest)) if is_folio(head) => rest.trim_start(), + _ => match out.rsplit_once(' ') { + Some((rest, tail)) if is_folio(tail) => rest.trim_end(), + _ => return out, + }, + }; + if trimmed == out { + return out; + } + out = trimmed; + } +} + +/// Lines that are printed matter rather than anything a book is called. +/// +/// Every one of these was read as a title off a real book in a real library: +/// a copyright notice, a translator's credit, a part heading in brackets, a +/// span of HTML a converter left behind, and a volume entry from a collected +/// edition's own list of volumes. +fn is_furniture(line: &str) -> bool { + let t = line.trim(); + if t.starts_with('[') || t.starts_with('\\') { + return true; + } + // A converter that left its markup in. Nothing printed on a title page is + // a tag, a stylesheet declaration, a markdown link or a filename, and a + // title that appears to be one is a fragment of the conversion rather than + // of the book. Rejecting only the first such line is no use: the next line + // of a mangled page is mangled too, so the whole shape has to go. + if t.contains(" Vec { @@ -214,7 +323,7 @@ pub fn read(lines: &[String]) -> Option { continue; } } - if is_structural(line) || BARE_DATE.is_match(line) { + if is_structural(line) || is_furniture(line) || BARE_DATE.is_match(line) { if block.is_empty() { continue; } @@ -289,3 +398,97 @@ pub fn read(lines: &[String]) -> Option { out.title.as_ref()?; Some(out) } + +#[cfg(test)] +mod furniture_tests { + use super::*; + + /// Every string here was read as a title off a book in a real library. + #[test] + fn printed_matter_is_not_a_title() { + for line in [ + "vi CONTENTS", + "viii CONTENTS", + "xiv ACKNOWLEDGEMENTS", + "12 INDEX", + "Translated from the French by Willard R. Trask", + "Copyright © 2013 by Barry Davies, BEM", + "[PART ONE:", + "Piper Verlag GmbH, Munich", + "Vol. 11. HENRY JAMES, JR.", + "All rights reserved", + "First published 1971", + ] { + assert!( + is_structural(line) || is_furniture(line), + "read as a title: {line:?}" + ); + } + } + + #[test] + fn a_real_title_survives_all_of_that() { + for line in [ + "The First World War", + "A Very Short Introduction", + "Christianity and Classical Culture", + "The Problems of Philosophy", + "Volume and Void", + "Contents of Consciousness", + "August 1914", + "Copyright and Its Discontents", + ] { + assert!( + !(is_structural(line) || is_furniture(line)), + "rejected a real title: {line:?}" + ); + } + } + + #[test] + fn a_folio_comes_off_either_end() { + assert_eq!(strip_folio("vi contents"), "contents"); + assert_eq!(strip_folio("contents vi"), "contents"); + assert_eq!(strip_folio("contents"), "contents"); + // A title that happens to be roman numerals throughout is not a folio + // with nothing left; it keeps whatever it had. + assert_eq!(strip_folio("i"), "i"); + } +} + +#[cfg(test)] +mod conversion_tests { + use super::*; + + /// A mangled page is mangled on every line, so rejecting the first + /// leftover only moves the misread down one. Each of these is the line + /// colophon fell through to after the line above it was rejected. + #[test] + fn what_a_converter_left_behind_is_never_a_title() { + for line in [ + "[PART ONE:", + "RULERS AND REDNECKS](Webb_9780767922951_epub_p01_r1.html)", + "[AFRICAN SLAVERY IN AMERICA - \\[1775\\]](index_split_004.html)", + "Piper Verlag GmbH, Munich