mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-10-23 11:17:13 +00:00
94 lines
3.6 KiB
Bash
94 lines
3.6 KiB
Bash
#!/bin/sh
|
|||
|
|
# A book file to passages worth embedding, and what its notes cite.
|
||
|
|
#
|
||
|
|
# The starting point is Gibbon's first volume from Project Gutenberg: public
|
||
|
|
# domain, a real table of contents, and a few hundred pages of footnotes.
|
||
|
|
#
|
||
|
|
# examples/shell/01-book-to-passages.sh [work-dir]
|
||
|
|
#
|
||
|
|
# Needs pandoc and jq, and the catena commands on PATH.
|
||
|
|
set -eu
|
||
|
|
|
||
|
|
work="${1:-./work}"
|
||
|
|
mkdir -p "$work"
|
||
|
|
book="$work/gibbon.epub"
|
||
|
|
|
||
|
|
if [ ! -s "$book" ]; then
|
||
|
|
echo "fetching Gibbon vol. 1 from Project Gutenberg"
|
||
|
|
curl -sSL --max-time 120 -o "$book" \
|
||
|
|
"https://www.gutenberg.org/cache/epub/731/pg731.epub"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# What the file actually is. Extensions lie, and a `.epub` that is really a
|
||
|
|
# zip of page scans fails four stages later, in a way that looks like a bug
|
||
|
|
# in the converter.
|
||
|
|
echo
|
||
|
|
echo "== what it is"
|
||
|
|
booksniff "$book" | jq -r '" \(.kind), \(.bytes) bytes"'
|
||
|
|
|
||
|
|
# A PDF gets one more question first: exit 0 from pdftriage means the text
|
||
|
|
# layer is not worth reading and the file has to be OCRed before any of this.
|
||
|
|
if [ "$(booksniff "$book" | jq -r .kind)" = "pdf" ] && pdftriage --quiet "$book"; then
|
||
|
|
echo " the text layer is garbled; OCR it before going on"
|
||
|
|
exit 0
|
||
|
|
fi
|
||
|
|
|
||
|
|
# What the file already says about itself — the publisher's own metadata,
|
||
|
|
# which beats anything guessed from a filename, and costs no lookup.
|
||
|
|
echo
|
||
|
|
echo "== what it says about itself"
|
||
|
|
bookmeta "$book" | jq -r '" \(.title)\n \(.authors | join("; "))\n \(.language // "?") · \(.date // "no date")"'
|
||
|
|
|
||
|
|
# The chapters the book declares, rather than any inferred from the text.
|
||
|
|
echo
|
||
|
|
echo "== the contents it declares"
|
||
|
|
tocfind "$book" --outline | sed -n '6,10p' | sed 's/^/ /'
|
||
|
|
echo " … $(tocfind "$book" | wc -l | tr -d ' ') entries"
|
||
|
|
|
||
|
|
# Convert, scrub, chunk. `commonmark` rather than `commonmark_x`, because the
|
||
|
|
# extended flavour writes anchors as bracketed spans and they end up in the
|
||
|
|
# passages. The scrub is what takes out the rest of the converter's leavings.
|
||
|
|
echo
|
||
|
|
echo "== passages"
|
||
|
|
pandoc -f epub -t commonmark --wrap=none "$book" -o "$work/book.md"
|
||
|
|
mdscrub < "$work/book.md" > "$work/clean.md"
|
||
|
|
chunkmd < "$work/clean.md" > "$work/chunks.ndjson"
|
||
|
|
|
||
|
|
jq -s -r '
|
||
|
|
" \(length) passages over \([.[].chapter] | unique | length) chapters",
|
||
|
|
" roles: \([.[].role] | group_by(.) | map("\(length) \(.[0])") | join(", "))",
|
||
|
|
" \([.[] | select(.flags | index("footnote_citations"))] | length) carry citation material"
|
||
|
|
' "$work/chunks.ndjson"
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo " a passage, with where it came from:"
|
||
|
|
jq -s -r '[.[] | select(.role == "body" and .chapter != null)][40]
|
||
|
|
| " [\(.idx)] \(.tokens) tokens · \(.chapter)\n \(.text[0:150])…"' \
|
||
|
|
"$work/chunks.ndjson"
|
||
|
|
|
||
|
|
# The notes. A passage flagged `footnote_citations` is a page of them, and
|
||
|
|
# each is read on its own — the flag says which passages are worth the ask,
|
||
|
|
# which is the whole reason chunkmd sets it.
|
||
|
|
echo
|
||
|
|
echo "== what the notes cite"
|
||
|
|
: > "$work/references.ndjson"
|
||
|
|
jq -c 'select(.flags | index("footnote_citations")) | .text' "$work/chunks.ndjson" \
|
||
|
|
| while IFS= read -r line; do
|
||
|
|
printf '%s' "$line" | jq -r . | citefind >> "$work/references.ndjson" || true
|
||
|
|
done
|
||
|
|
|
||
|
|
jq -s -r '
|
||
|
|
" \(length) entries read",
|
||
|
|
" \([.[] | select(.title)] | length) name a work, \([.[] | select(.authors | length > 0)] | length) name an author",
|
||
|
|
" \([.[] | select(.unsettled | length > 0)] | length) came back with something unread"
|
||
|
|
' "$work/references.ndjson"
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo " Gibbon cites in the classical short form — \"Tacit. Annal. xiii. 5.\" —"
|
||
|
|
echo " so most entries name the passage rather than a book, and say so:"
|
||
|
|
jq -r 'select(.unsettled | length > 0) | " \(.text[0:58]) → unread: \(.unsettled | join(", "))"' \
|
||
|
|
"$work/references.ndjson" | head -4
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo "written to $work/"
|