diff --git a/crates/bindings/README.md b/crates/bindings/README.md index 8907f33..75f2bf1 100644 --- a/crates/bindings/README.md +++ b/crates/bindings/README.md @@ -79,6 +79,7 @@ except ValueError as why: | `series_of` | [`seriesof`](../seriesof/README.md) | | `parse_identifier` | [`idnorm`](../idnorm/README.md) | | `find_citations` | [`citefind`](../citefind/README.md) | +| `looks_like_footnote_citations` | whether a body chunk carries notes worth reading | | `citation_prompt` | the question `citefind --prompt` writes | | `merge_citation_reply` | what `citefind --merge` does with the answer | diff --git a/crates/bindings/include/catena.h b/crates/bindings/include/catena.h index 2a1f18d..8a65bff 100644 --- a/crates/bindings/include/catena.h +++ b/crates/bindings/include/catena.h @@ -98,6 +98,10 @@ char *catena_series_of(const char *title, const char *author); * one — an answer, not an error. */ char *catena_parse_identifier(const char *text); +/* Whether a chunk of body text carries footnote or endnote citations. + * Returns 1 for yes, 0 for no, -1 on error. */ +int catena_looks_like_footnote_citations(const char *text); + /* Read the references out of a bibliography or a page of notes. Returns a * JSON array, one object per reference. */ char *catena_find_citations(const char *text); diff --git a/crates/bindings/python/catena/__init__.py b/crates/bindings/python/catena/__init__.py index 9199aba..00d6593 100644 --- a/crates/bindings/python/catena/__init__.py +++ b/crates/bindings/python/catena/__init__.py @@ -19,6 +19,7 @@ from ._catena import ( find_citations, fuse_hits, identify, + looks_like_footnote_citations, merge_citation_reply, pack_cues, parse_identifier, @@ -38,6 +39,7 @@ __all__ = [ "find_citations", "fuse_hits", "identify", + "looks_like_footnote_citations", "merge_citation_reply", "pack_cues", "parse_identifier", diff --git a/crates/bindings/python/catena/__init__.pyi b/crates/bindings/python/catena/__init__.pyi index 6e850aa..6c09d32 100644 --- a/crates/bindings/python/catena/__init__.pyi +++ b/crates/bindings/python/catena/__init__.pyi @@ -38,6 +38,7 @@ def fuse_hits( ) -> list[dict[str, Any]]: ... 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 citation_prompt(references: Iterable[Mapping[str, Any]]) -> str | None: ... def merge_citation_reply( references: Iterable[Mapping[str, Any]], reply: str diff --git a/crates/bindings/src/capi.rs b/crates/bindings/src/capi.rs index cd30b11..c304185 100644 --- a/crates/bindings/src/capi.rs +++ b/crates/bindings/src/capi.rs @@ -417,6 +417,21 @@ pub unsafe extern "C" fn catena_identify( give_json(&crate::identify(name, bytes)) } +/// Whether a chunk of body text carries footnote or endnote citations. +/// Returns 1 for yes, 0 for no, -1 on error. +/// +/// # Safety +/// +/// `text` must be NUL-terminated UTF-8. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn catena_looks_like_footnote_citations(text: *const c_char) -> i32 { + clear_error(); + let Some(text) = (unsafe { borrow(text, "text") }) else { + return -1; + }; + i32::from(crate::looks_like_footnote_citations(text)) +} + /// Read the references out of a bibliography or a page of notes. Returns a /// JSON array, one object per reference. /// diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index 758c302..e68889e 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -215,6 +215,14 @@ pub fn parse_identifier(text: &str) -> Option { idnorm::parse(text) } +/// Whether a chunk of body text carries footnote or endnote citations. +/// +/// The structural signal a converter leaves on EPUB3 note definitions first; +/// otherwise the numbered-note markers and what sits after them. +pub fn looks_like_footnote_citations(text: &str) -> bool { + chunkmd::footnote::looks_like_citations(text) +} + /// Read the references out of a bibliography or a page of notes. /// /// Each carries `unsettled`: the fields that could not be read off the entry, @@ -261,6 +269,18 @@ mod tests { assert_eq!(count_tokens("a passage"), textkit::count("a passage")); } + #[test] + fn the_footnote_signal_agrees_with_the_library_underneath() { + const NOTES: &str = "1. John Smith, *A Title* (Oxford: Clarendon Press, 1998), 183.\n\ + 2. Jane Doe, *Another* (Cambridge: University Press, 2001), p. 12."; + assert!(looks_like_footnote_citations(NOTES)); + assert_eq!( + looks_like_footnote_citations(NOTES), + chunkmd::footnote::looks_like_citations(NOTES) + ); + assert!(!looks_like_footnote_citations("Just a paragraph of prose.")); + } + #[test] fn the_citation_surface_agrees_with_the_library_underneath() { const REFS: &str = "Smith, John. *A Title*. Oxford: Clarendon Press, 1998.\n\nMarron, Kevin Ritual Abuse, Seal Books, 1988"; diff --git a/crates/bindings/src/python.rs b/crates/bindings/src/python.rs index a2c2d11..0421cdf 100644 --- a/crates/bindings/src/python.rs +++ b/crates/bindings/src/python.rs @@ -220,6 +220,12 @@ fn identify<'py>(py: Python<'py>, name: &str, data: &[u8]) -> PyResult bool { + crate::looks_like_footnote_citations(text) +} + /// Read the references out of a bibliography or a page of notes. #[pyfunction] fn find_citations<'py>(py: Python<'py>, text: &str) -> PyResult> { @@ -290,6 +296,7 @@ fn _catena(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(fuse_hits, module)?)?; module.add_function(wrap_pyfunction!(identify, module)?)?; module.add_function(wrap_pyfunction!(find_citations, module)?)?; + module.add_function(wrap_pyfunction!(looks_like_footnote_citations, 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)?)?; diff --git a/crates/bindings/src/wasm.rs b/crates/bindings/src/wasm.rs index 8c50a43..dcbb45d 100644 --- a/crates/bindings/src/wasm.rs +++ b/crates/bindings/src/wasm.rs @@ -305,6 +305,12 @@ pub fn identify(name: &str, data: &[u8]) -> Result { to_js(&crate::identify(name, data)) } +/// Whether a chunk of body text carries footnote or endnote citations. +#[wasm_bindgen(js_name = looksLikeFootnoteCitations)] +pub fn looks_like_footnote_citations(text: &str) -> bool { + crate::looks_like_footnote_citations(text) +} + /// Read the references out of a bibliography or a page of notes. #[wasm_bindgen(js_name = findCitations, skip_typescript)] pub fn find_citations(text: &str) -> Result {