mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-10-22 18:57:14 +00:00
seriesof: the series a book belongs to, and which volume this is
This commit is contained in:
@@ -36,6 +36,7 @@ cuepack.workspace = true
|
||||
mdscrub.workspace = true
|
||||
mdsplit.workspace = true
|
||||
rrf.workspace = true
|
||||
seriesof.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
textkit.workspace = true
|
||||
|
||||
@@ -67,6 +67,7 @@ except ValueError as why:
|
||||
| `split_collection` | [`mdsplit`](../mdsplit/README.md) |
|
||||
| `fuse_hits` | [`rrf`](../rrf/README.md) |
|
||||
| `identify` | [`booksniff`](../booksniff/README.md) |
|
||||
| `series_of` | [`seriesof`](../seriesof/README.md) |
|
||||
|
||||
The tools that are not here are the ones a binding would gain nothing over a
|
||||
subprocess for: they read files, shell out to another program, or talk to a
|
||||
|
||||
@@ -48,6 +48,7 @@ try {
|
||||
| `splitCollection` | One markdown file into the works inside it |
|
||||
| `fuseHits` | Several ranked lists into one |
|
||||
| `identify` | What a file is, from its first bytes |
|
||||
| `seriesOf` | The series a book belongs to, and which volume this is |
|
||||
|
||||
The tools that read files or call another program are not here: a browser has
|
||||
neither, and a binding that pretends otherwise fails at run time rather than
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// check, and nothing else would notice.
|
||||
import {
|
||||
chunkMarkdown, scrub, countTokens, parseSubtitles, packCues,
|
||||
splitCollection, fuseHits, identify,
|
||||
type Chunk, type Cue, type Section, type Hit, type Doc,
|
||||
splitCollection, fuseHits, identify, seriesOf,
|
||||
type Chunk, type Cue, type Section, type Hit, type Doc, type SeriesProposal,
|
||||
} from "./dist/web/catena.js";
|
||||
|
||||
const chunks: Chunk[] = chunkMarkdown("# One\n\nbody\n", 512, 64, 50, 768);
|
||||
@@ -35,4 +35,8 @@ parseSubtitles("x");
|
||||
packCues(cues);
|
||||
fuseHits(hits);
|
||||
|
||||
export const checked = [page, chapter, flags, clean, tokens, start, packed, slug, fused, kind, notes];
|
||||
const proposal: SeriesProposal | undefined = seriesOf("The Philokalia Vol 3");
|
||||
const volume: number | undefined = proposal?.volume;
|
||||
seriesOf("Aquinas (Companion)", "Cambridge");
|
||||
|
||||
export const checked = [volume, page, chapter, flags, clean, tokens, start, packed, slug, fused, kind, notes];
|
||||
|
||||
@@ -19,6 +19,7 @@ from ._catena import (
|
||||
pack_cues,
|
||||
parse_subtitles,
|
||||
scrub,
|
||||
series_of,
|
||||
split_collection,
|
||||
)
|
||||
|
||||
@@ -31,5 +32,6 @@ __all__ = [
|
||||
"pack_cues",
|
||||
"parse_subtitles",
|
||||
"scrub",
|
||||
"series_of",
|
||||
"split_collection",
|
||||
]
|
||||
|
||||
@@ -30,3 +30,6 @@ def fuse_hits(
|
||||
limit: int | None = ...,
|
||||
) -> list[dict[str, Any]]: ...
|
||||
def identify(name: str, data: bytes) -> dict[str, Any]: ...
|
||||
def series_of(
|
||||
title: str, author: str | None = ...
|
||||
) -> dict[str, Any] | None: ...
|
||||
|
||||
@@ -356,6 +356,35 @@ pub unsafe extern "C" fn catena_identify(
|
||||
give_json(&crate::identify(name, bytes))
|
||||
}
|
||||
|
||||
/// The series a title belongs to. Returns a JSON proposal, or NULL when the
|
||||
/// title says nothing about a series — which is most titles, and not an
|
||||
/// error: `catena_last_error` is NULL in that case.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `title` must be NUL-terminated UTF-8; `author` likewise, or NULL.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn catena_series_of(
|
||||
title: *const c_char,
|
||||
author: *const c_char,
|
||||
) -> *mut c_char {
|
||||
clear_error();
|
||||
let Some(title) = (unsafe { borrow(title, "title") }) else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
let author = match author.is_null() {
|
||||
true => None,
|
||||
false => match unsafe { borrow(author, "author") } {
|
||||
Some(author) => Some(author),
|
||||
None => return ptr::null_mut(),
|
||||
},
|
||||
};
|
||||
match crate::series_of(title, author) {
|
||||
Some(found) => give_json(&found),
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -455,6 +484,18 @@ mod tests {
|
||||
assert!(chunks.contains("the mill stood at the northern end"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_title_with_no_series_is_null_and_not_an_error() {
|
||||
let title = c"Being and Time";
|
||||
assert!(through(|| unsafe { catena_series_of(title.as_ptr(), ptr::null()) }).is_none());
|
||||
assert_eq!(error(), None, "no series is an answer, not a failure");
|
||||
|
||||
let series = c"The Philokalia Vol 3";
|
||||
let json = through(|| unsafe { catena_series_of(series.as_ptr(), ptr::null()) })
|
||||
.expect("a proposal");
|
||||
assert!(json.contains("\"volume\":3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_abi_version_is_the_one_the_header_declares() {
|
||||
assert_eq!(catena_abi_version(), 1);
|
||||
|
||||
@@ -176,6 +176,11 @@ pub fn identify(name: &str, bytes: &[u8]) -> Doc {
|
||||
booksniff::sniff(&bytes, name)
|
||||
}
|
||||
|
||||
/// The series a title belongs to, and which volume of it this is.
|
||||
pub fn series_of(title: &str, author: Option<&str>) -> Option<seriesof::Proposal> {
|
||||
seriesof::series_of(title, author)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -232,6 +237,14 @@ mod tests {
|
||||
assert_eq!(channel_named("lexical"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_series_comes_back_the_same_as_the_library_reads_it() {
|
||||
let found = series_of("The Philokalia Vol 3", None).expect("a series");
|
||||
assert_eq!(found.series, "The Philokalia");
|
||||
assert_eq!(found.volume, Some(3));
|
||||
assert_eq!(series_of("Being and Time", None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_are_identified_the_same_way_a_file_is() {
|
||||
let pdf = b"%PDF-1.4\n";
|
||||
|
||||
@@ -192,6 +192,22 @@ fn identify<'py>(py: Python<'py>, name: &str, data: &[u8]) -> PyResult<Bound<'py
|
||||
to_py(py, &crate::identify(name, data))
|
||||
}
|
||||
|
||||
/// The series a title belongs to, and which volume of it this is.
|
||||
///
|
||||
/// `None` when the title says nothing about a series, which is most titles.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (title, author = None))]
|
||||
fn series_of<'py>(
|
||||
py: Python<'py>,
|
||||
title: &str,
|
||||
author: Option<&str>,
|
||||
) -> PyResult<Option<Bound<'py, PyAny>>> {
|
||||
match crate::series_of(title, author) {
|
||||
Some(found) => Ok(Some(to_py(py, &found)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// The module Python imports.
|
||||
#[pymodule]
|
||||
fn _catena(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
@@ -204,5 +220,6 @@ fn _catena(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
module.add_function(wrap_pyfunction!(split_collection, module)?)?;
|
||||
module.add_function(wrap_pyfunction!(fuse_hits, module)?)?;
|
||||
module.add_function(wrap_pyfunction!(identify, module)?)?;
|
||||
module.add_function(wrap_pyfunction!(series_of, module)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -70,6 +70,20 @@ export interface Hit {
|
||||
payload: unknown;
|
||||
}
|
||||
|
||||
/** What a title says about its series. */
|
||||
export interface SeriesProposal {
|
||||
v: number;
|
||||
title: string;
|
||||
author?: string;
|
||||
series: string;
|
||||
/** Which volume, where the title said. Absent for membership without order. */
|
||||
volume?: number;
|
||||
/** A title reconstructed from publisher shorthand, where one reliably can be. */
|
||||
proposed_title?: string;
|
||||
/** "volume" or "publisher". */
|
||||
method: string;
|
||||
}
|
||||
|
||||
/** What a file is. */
|
||||
export interface Doc {
|
||||
v: number;
|
||||
@@ -208,6 +222,17 @@ pub fn identify(name: &str, data: &[u8]) -> Result<JsValue, JsValue> {
|
||||
to_js(&crate::identify(name, data))
|
||||
}
|
||||
|
||||
/// The series a title belongs to, and which volume of it this is.
|
||||
///
|
||||
/// `undefined` when the title says nothing about a series.
|
||||
#[wasm_bindgen(js_name = seriesOf, skip_typescript)]
|
||||
pub fn series_of(title: &str, author: Option<String>) -> Result<JsValue, JsValue> {
|
||||
match crate::series_of(title, author.as_deref()) {
|
||||
Some(found) => to_js(&found),
|
||||
None => Ok(JsValue::UNDEFINED),
|
||||
}
|
||||
}
|
||||
|
||||
/// Declarations for the functions whose return type the generator cannot
|
||||
/// infer, so a caller gets `Chunk[]` rather than `any`.
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
@@ -218,4 +243,5 @@ export function packCues(cues: Cue[], seconds?: number, max_tokens?: number): Ch
|
||||
export function splitCollection(markdown: string): Section[];
|
||||
export function fuseHits(hits: Hit[], k?: number, weights?: Record<string, number>, limit?: number): Hit[];
|
||||
export function identify(name: string, data: Uint8Array): Doc;
|
||||
export function seriesOf(title: string, author?: string): SeriesProposal | undefined;
|
||||
"#;
|
||||
|
||||
Reference in New Issue
Block a user