# 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](https://www.loc.gov/aba/pcc/053/table.html),
[CSM G 63](https://www.loc.gov/aba/publications/FreeCSM/G063.pdf)),
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
| `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.)
```rust
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:
| 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](https://github.com/veralvx/csan/releases), or the Python
package below:
```sh
$ 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:
```sh
pip install csan # library + CLI
uv tool install csan # just the CLI, isolated
```
```python
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`](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`:
```js
import { cutterLoc, cutterSanborn } from "csan-web";
cutterSanborn({ surname: "Dickens", title: "Oliver Twist" }); // "D548o"
cutterLoc({ surname: "Carter" }); // "C378"
```
See [`crates/csan-web/README.md`](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:
```typ
// 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](https://github.com/typst/packages#local-packages): 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:
```sh
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](https://opensource.axo.dev/cargo-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](https://pypi.org/project/csan/) (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](LICENSE-APACHE) or [MIT license](LICENSE-MIT) at your option.