Files
catena/crates/mdsplit/README.md
T

175 lines
5.4 KiB
Markdown
Raw Normal View History

# mdsplit
Splits one markdown file into the works inside it.
Part of [catena](../../README.md).
## What it does
Finds the seams in a collection and cuts along them, writing one record per
work — or one file per work.
A document that is simply one work is not a collection, and says so rather
than pretending to split.
## When you would use it
- **Before chunking a collected edition.** Forty books in one file, indexed
whole, means a query for any of them retrieves a passage that could be from
any other, and the title on the result names the collection rather than the
work.
- **Making a collection citable.** Each section gets its own identifier, so a
passage can be attributed to the work it is actually from.
- **Scripture**, which needs its own rule and gets one.
## Install
```sh
cargo install --git https://git.pumpin.online/funkpower/catena mdsplit
```
## Example
A collection of twelve works:
```
$ mdsplit works.md
{"v":1,"idx":0,"title":"Work 1","slug":"work-1","text":"# Work 1\n\nThe obligation ran both ways…"}
{"v":1,"idx":1,"title":"Work 2","slug":"work-2","text":"# Work 2\n\nThe obligation ran both ways…"}
{"v":1,"idx":2,"title":"Work 3","slug":"work-3","text":"# Work 3\n\nThe obligation ran both ways…"}
```
Scripture, grouped by book rather than by chapter:
```
$ mdsplit bible.md | jq -r '"\(.idx)\t\(.title)\t\(.text | length) chars"'
0 Genesis 29929 chars
1 Exodus 29914 chars
2 Leviticus 29959 chars
3 Numbers 29929 chars
4 Deuteronomy 29989 chars
5 Joshua 29914 chars
```
Ninety chapter headings became six books. Splitting on the headings
themselves would have given ninety fragments, none of them a work.
One work is not a collection:
```
$ mdsplit politics.md
mdsplit: one heading level gave 1 sections (only 1 sections, fewer than 5); the next gave 0 (only 0 sections, fewer than 5)
$ echo $?
1
```
Writing files instead of records:
```
$ mdsplit --into sections/ bible.md
sections/000-genesis.md
sections/001-exodus.md
sections/002-leviticus.md
…
```
In a pipeline:
```sh
mdsplit --into sections/ complete-works.md
for f in sections/*.md; do mdscrub "$f" | chunkmd > "chunks/$(basename "$f" .md).ndjson"; done
```
## Options
| Option | Effect |
|---|---|
| `--into <DIR>` | Write one file per section instead of records |
| `--man` | Write this page as a manual page, in roff |
Files are named for their position and their title, so a directory listing is
in reading order.
## Exit codes
| Code | Meaning |
|---|---|
| `0` | It split |
| `1` | It is not a collection |
| `64` | The command line was wrong |
| `65` | The input was not text |
| `74` | The file could not be read |
Exit `1` is an answer, not a failure — most documents are one work, and the
diagnostic on stderr says what each heading level produced.
## How it decides
Three candidates in order — scripture, top-level headings, the level below —
and the first plausible one wins.
A candidate is plausible unless:
- it found **fewer than five sections**, which means the document has no
collection structure;
- it found **more than fifteen hundred**, which means it found structure that
is not there;
- the **typical section is under 1,250 characters**, which means it is cutting
at the line level rather than at the work level.
The median is used rather than the average, because one enormous work among
sixty ordinary ones is a real shape that an average would hide.
Otherwise it is permissive on purpose. Collections vary wildly: one giant work
among sixty, five hundred short homilies, a hundred and eighty distinct works.
### Two details worth knowing
**Whatever sits above the first heading** — a contents page, an
acknowledgement — goes onto the first section rather than being dropped. It is
usually the only place a collection describes itself.
**A heading with nothing under it folds into the next one.** Anthologies emit
them in pairs — `# CHAPTER I` immediately followed by `# THE WORK` — and the
second owns the content. The stub's own text is kept, so the chapter number is
still searchable as context.
## Scripture
Recognised when a document has at least thirty top-level headings and more
than forty per cent of them name a book.
The book names are general reference data rather than anything local:
Septuagint primaries with the English and Protestant spellings as variants,
because an edition uses one convention throughout and both have to resolve to
the same book. `1 Samuel` and `1 Kingdoms` are one section. So are
`The First Book of the Kingdoms`, `Annotations for Job` (which belongs with
Job), `Tobit†`, and `Susanna` (which belongs with Daniel).
Books come out in canonical order whatever order the edition printed them in,
and a heading that names no book — a preface, a table of weights — becomes its
own section and sorts after the canon.
An edition that interrupts itself still gets one section per book: a prologue
between a book's introduction and its first chapter would otherwise make that
book appear twice, as two separate works.
## As a library
```rust
match mdsplit::split(&markdown) {
Ok(sections) => for section in sections {
println!("{} {}", section.idx, section.title);
},
Err(why) => eprintln!("not a collection: {why}"),
}
```
`mdsplit::bible::classify` is usable on its own, for anything that needs to
know which book a heading names.
## See also
- [`mdscrub`](../mdscrub/README.md) — what to run on each section afterwards.
- [`chunkmd`](../chunkmd/README.md) — turning each work into passages.