csan-core 0.6.0

Cutter call number generation (Cutter-Sanborn and Library of Congress) for library cataloging
Documentation

csan

Cutter call numbers for library cataloging, as a Rust library and a Typst plugin.

csan derives shelf-ready cutter numbers from author names (or, for anonymous works, titles) using two methods:

  • Cutter-Sanborn three-figure — a lookup against the bundled Cutter-Sanborn table (12,000+ entries), with an optional call number prefix and a lowercase work mark derived from the title.
  • Library of Congress — the LoC character-branching rules (Cutter table, CSM G 63), producing an initial plus a digit sequence.

Input names are normalized first: whitespace and punctuation are stripped, diacritics and ligatures are folded to ASCII (Čapek → Capek, Strauß → Strauss), Mc/M' prefixes expand to Mac, and St/Ste expand to Saint/Sainte, matching the conventions of the underlying tables.

Workspace layout

Path Contents
crates/csan-core The library: normalization, models, generators, the table.
crates/csan The csan command-line tool (clap).
crates/csan-typst The Typst WASM plugin wrapping the library.
crates/csan-typst/pkg The Typst package (typst.toml, lib.typ, plugin binary).
crates/csan-py Python bindings and the PyPI package (pyo3 + maturin).
crates/csan-web JavaScript/npm bindings via WebAssembly (wasm-bindgen).

Rust usage

Add the library with cargo add csan-core. (Up to 0.5.x the library was published as csan; that name now belongs to the command-line tool.)

use csan_core::{
    Attribution, Author, CutterConfig, CutterGenerator, CutterLocMethod,
    CutterSanbornMethod, Opus,
};

let entry = Attribution::Both {
    author: Author::new("Charles", "Dickens"),
    opus: Opus::new("Oliver Twist"),
};

let sanborn = CutterSanbornMethod::generate(&entry, CutterConfig::default())?;
assert_eq!(sanborn.to_string(), "D548o");

let loc = CutterLocMethod::generate(&entry, CutterConfig::default())?;
assert_eq!(loc.to_string(), "D535");
# Ok::<(), csan_core::NormalizationError>(())

CutterConfig tunes the output: prefix_len and suffix_len control the Cutter-Sanborn prefix and work mark, and max_len controls the LoC digit count (clamped to 4). Works without an author use Attribution::OpusOnly(...) and are cuttered by title, including the LoC numeral rule ("100 best recipes" → A153).

Expected values

Generated by cargo run --example showcase with default configuration:

Author Cutter-Sanborn Library of Congress
Jane Austen A933 A978
Eric Blair B635 B535
Agatha Christie C555 C475
Samuel Clemens C625 C546
Jorge De la Cruz D332 D453
Charles Dickens D548 D535
Emily Dickinson D553 D535
Fyodor Dostoyevsky D724 D678
Stephen King K54 K564
VERA LVX L979 L89
Herman Melville M531 M458
George Orwell O79 O794
William Shakespeare S527 S535
Lord Sith S623 S584
Ivan Smith S649 S658
William Smith S664 S658
Leo Tolstoy T654 T657
Mark Twain T969 T835
Virginia Woolf W913 W665
Emile Zola Z86 Z653

Command line

The csan crate (crates/csan) provides the csan binary. Install it with cargo install csan, the shell/PowerShell installers or archives attached to each GitHub release, or the Python package below:

$ csan --cutter-sanborn --cutter-loc \
    --forename Charles --surname Dickens --title "Oliver Twist"
D548o
D535

Select at least one method (--cutter-sanborn, --cutter-loc) and provide at least one non-blank input (--forename, --surname, --title). --prefix-len/--suffix-len tune the Cutter-Sanborn output, --max-len the LoC digits, and --json emits the parts as a JSON object. Errors exit non-zero with a message on stderr.

Python

crates/csan-py packages the library for Python (CPython 3.9+, abi3 wheels) under the PyPI name csan, including the CLI as a console script:

pip install csan        # library + CLI
uv tool install csan    # just the CLI, isolated
import csan
csan.cutter_sanborn(surname="Dickens", title="Oliver Twist")  # 'D548o'
csan.cutter_loc_detailed(surname="Carter")
# {'alpha_prefix': 'C', 'numeric': 378, 'formatted': 'C378'}

See crates/csan-py/README.md for the full API and the maturin build/publish flow.

JavaScript (npm)

crates/csan-web exposes the library to JavaScript via WebAssembly, packaged for npm with wasm-pack:

import { cutterLoc, cutterSanborn } from "csan-web";
cutterSanborn({ surname: "Dickens", title: "Oliver Twist" }); // "D548o"
cutterLoc({ surname: "Carter" }); // "C378"

See crates/csan-web/README.md for build, examples (Node and browser), and npm publishing instructions.

Typst usage

The Typst package lives in crates/csan-typst/pkg and exposes two functions:

// Replace <version> with the installed release.
#import "@local/csan:<version>": cutter-sanborn, cutter-loc

#cutter-sanborn(
  forename: "Charles",
  surname: "Dickens",
  title: "Oliver Twist",
) // -> "D548o"

#cutter-loc(surname: "Carter") // -> "C378"

Both accept forename, surname, and title (at least one of author/title must be non-blank; blank strings count as absent). cutter-sanborn also takes prefix-len and suffix-len; cutter-loc takes max-len. Pass detailed: true to receive a dictionary of the parts (alpha_prefix, numeric, …) instead of the formatted string. Requires Typst 0.14.2 or later.

Until the package is published to Typst Universe, install it as a local package: extract the csan_typst-<version>.tar.gz attached to each GitHub release into {data-dir}/typst/packages/local/csan/<version>/ (or copy crates/csan-typst/pkg there), then import @local/csan:<version>. The plugin binary alone is attached as csan_typst.wasm.

Building the plugin

The compiled plugin (csan_typst.wasm) is committed alongside the package, so Typst users don't need a Rust toolchain. To rebuild it after changing the Rust code:

rustup target add wasm32-unknown-unknown
just wasm        # build + install into crates/csan-typst/pkg
just wasm-opt    # optional: shrink with binaryen's wasm-opt
just typst-test  # compile the assert-based plugin test suite

Releasing

just release <major|minor|patch> (on an up-to-date, clean main) bumps the workspace version, the csan-core dependency pin, and the Typst package version, prepends the release's section to CHANGELOG.md with git-cliff, then commits, tags, and pushes. It refuses a bump smaller than the unreleased breaking changes require. The tag starts the Release workflow, which uses dist to build the csan binaries and installers and attaches, to the same GitHub release:

  • the Typst plugin (csan_typst.wasm) and package (csan_typst-<version>.tar.gz);
  • the Python wheels (abi3, Linux/musllinux/macOS/Windows on x86_64 and aarch64) and sdist;
  • the Node npm package (csan-web-<version>.tgz).

Once the release succeeds, the Publish to PyPI workflow uploads its wheels and sdist to PyPI (trusted publishing; the one-time setup is described in .github/workflows/publish-pypi.yml). Crates are published to crates.io by hand, library first: cargo publish -p csan-core && cargo publish -p csan.

Development

The repository pins Rust 1.97 (rust-toolchain.toml). just checks runs the core CI gates: cargo check/test/clippy -D warnings (all targets, with the cargo, pedantic, and restriction clippy groups enabled), cargo fmt --check, cargo doc -D warnings, cargo audit, dprint check, and cog check for Conventional Commits. CI additionally builds and smoke-tests the Typst plugin, the Python wheel, and the npm package. Lint policy — including the reason for every allowed lint — lives in the workspace Cargo.toml and clippy.toml.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.