diff --git a/crates/bindings/README.md b/crates/bindings/README.md index cfb17c1..23aa19f 100644 --- a/crates/bindings/README.md +++ b/crates/bindings/README.md @@ -81,6 +81,9 @@ except ValueError as why: | `find_citations` | [`citefind`](../citefind/README.md) | | `looks_like_footnote_citations` | whether a body chunk carries notes worth reading | | `suggest_author` | [`bibclean`](../bibclean/README.md)'s author recovery | +| `canonical_subject` | what a closed subject scheme calls a heading | +| `compose_subjects` | the subjects a book should carry under one | +| `needs_subject` | whether it carries a heading the scheme knows | | `citation_prompt` | the question `citefind --prompt` writes | | `merge_citation_reply` | what `citefind --merge` does with the answer | diff --git a/crates/bindings/python/catena/__init__.py b/crates/bindings/python/catena/__init__.py index a8bbccd..6feead9 100644 --- a/crates/bindings/python/catena/__init__.py +++ b/crates/bindings/python/catena/__init__.py @@ -13,14 +13,17 @@ lines of JSON. from ._catena import ( __version__, cue_windows, + canonical_subject, chunk_markdown, citation_prompt, + compose_subjects, count_tokens, find_citations, fuse_hits, identify, looks_like_footnote_citations, merge_citation_reply, + needs_subject, pack_cues, parse_identifier, parse_subtitles, @@ -34,14 +37,17 @@ from ._catena import ( __all__ = [ "__version__", "cue_windows", + "canonical_subject", "chunk_markdown", "citation_prompt", + "compose_subjects", "count_tokens", "find_citations", "fuse_hits", "identify", "looks_like_footnote_citations", "merge_citation_reply", + "needs_subject", "pack_cues", "parse_identifier", "parse_subtitles", diff --git a/crates/bindings/python/catena/__init__.pyi b/crates/bindings/python/catena/__init__.pyi index b6c15aa..9d9770a 100644 --- a/crates/bindings/python/catena/__init__.pyi +++ b/crates/bindings/python/catena/__init__.pyi @@ -39,6 +39,17 @@ def fuse_hits( def identify(name: str, data: bytes) -> dict[str, Any]: ... def find_citations(text: str) -> list[dict[str, Any]]: ... def looks_like_footnote_citations(text: str) -> bool: ... +def canonical_subject( + scheme: Mapping[str, Any], name: str +) -> str | None: ... +def compose_subjects( + scheme: Mapping[str, Any], + subjects: Iterable[str], + title: str | None = ..., +) -> list[str]: ... +def needs_subject( + scheme: Mapping[str, Any], existing: Iterable[str] +) -> bool: ... def suggest_author( title: str, front_matter: str | None = ... ) -> dict[str, Any] | None: ... diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index ee58c30..5d4fcde 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -215,6 +215,30 @@ pub fn parse_identifier(text: &str) -> Option { idnorm::parse(text) } +/// What a closed subject scheme says about a set of headings. +/// +/// The scheme is supplied rather than built in — which subjects a collection +/// uses is the most collection-specific thing there is. These take it as the +/// record `bibclean` reads from a file. +pub fn canonical_subject(scheme: &bibclean::scheme::Scheme, name: &str) -> Option { + scheme.canonical(name).map(str::to_string) +} + +/// The subjects a book should carry: the chosen ones, their parents, and +/// nothing the title rules out. +pub fn compose_subjects( + scheme: &bibclean::scheme::Scheme, + subjects: &[String], + title: Option<&str>, +) -> Vec { + scheme.compose(subjects, title) +} + +/// Whether a book has no heading the scheme recognises. +pub fn needs_subject(scheme: &bibclean::scheme::Scheme, existing: &[String]) -> bool { + scheme.needs_subject(existing) +} + /// The best author a record's own title and front matter can offer. /// /// `None` when neither says anything that reads as a person's name, which is diff --git a/crates/bindings/src/python.rs b/crates/bindings/src/python.rs index dc97a1b..0ea49e7 100644 --- a/crates/bindings/src/python.rs +++ b/crates/bindings/src/python.rs @@ -220,6 +220,29 @@ fn identify<'py>(py: Python<'py>, name: &str, data: &[u8]) -> PyResult, name: &str) -> PyResult> { + Ok(crate::canonical_subject(&from_py(scheme)?, name)) +} + +/// The subjects a book should carry under a scheme. +#[pyfunction] +#[pyo3(signature = (scheme, subjects, title = None))] +fn compose_subjects( + scheme: &Bound<'_, PyAny>, + subjects: Vec, + title: Option<&str>, +) -> PyResult> { + Ok(crate::compose_subjects(&from_py(scheme)?, &subjects, title)) +} + +/// Whether a book carries no heading the scheme recognises. +#[pyfunction] +fn needs_subject(scheme: &Bound<'_, PyAny>, existing: Vec) -> PyResult { + Ok(crate::needs_subject(&from_py(scheme)?, &existing)) +} + /// The best author a record's own title and front matter can offer. /// /// `None` when neither says anything that reads as a person's name. @@ -314,6 +337,9 @@ fn _catena(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(find_citations, module)?)?; module.add_function(wrap_pyfunction!(looks_like_footnote_citations, module)?)?; module.add_function(wrap_pyfunction!(suggest_author, module)?)?; + module.add_function(wrap_pyfunction!(needs_subject, module)?)?; + module.add_function(wrap_pyfunction!(compose_subjects, module)?)?; + module.add_function(wrap_pyfunction!(canonical_subject, module)?)?; module.add_function(wrap_pyfunction!(citation_prompt, module)?)?; module.add_function(wrap_pyfunction!(merge_citation_reply, module)?)?; module.add_function(wrap_pyfunction!(series_of, module)?)?;