package chunk
import (
"bytes"
"encoding/xml"
"net/url"
"path"
"sort"
"strings"
"golang.org/x/net/html"
)
// The EPUB table of contents, and why the spine is not one.
//
// A spine states READING ORDER: the sequence a reader is walked through. A nav (EPUB 3) or an NCX (EPUB 2)
// states the CHAPTER STRUCTURE the book itself claims. Reading the spine as a chapter list is an assumption
// — right for the common one-document-per-chapter build, wrong for the many that split a chapter across
// documents or pack several into one, and wrong for every service page that rides the spine.
//
// Precedence is nav → NCX → spine, and a nav whose targets all fail to resolve counts as ABSENT rather than
// as an empty table of contents: a broken file must fall through to the next witness, not produce zero
// chapters.
// tocKind names which witness drew the boundaries, so the caller can report provenance without re-deriving
// it.
type tocKind int
const (
tocNone tocKind = iota
tocNav
tocNCX
)
// epubSpineDoc is one entry of the spine, after the manifest has been resolved.
type epubSpineDoc struct {
idref string
entry string // resolved zip path
linear bool
// service marks a document that is not book text: the nav document itself, or one the book declares as
// its toc or cover. Excluded from the reading WITH a count.
service bool
// readable is false for a spine item that is not an (x)html content document (an image, a stylesheet).
readable bool
}
// epubTOC is a resolved table of contents: the spine indices that START a chapter, already filtered,
// de-duplicated and ordered.
type epubTOC struct {
kind tocKind
// starts are spine indices, unique, kept ascending. Read as a SET — see resolveTOC.
starts []int
// unresolved counts targets that named NOTHING IN THE SPINE — a broken table of contents, which is a real
// property of scraped EPUBs and worth an alarm.
//
// ⚠ It deliberately does NOT count a target that resolved fine but landed on a service page or a
// non-linear one. Those are ordinary and expected — an EPUB 2 whose toc lists its own cover would report
// a "broken" table of contents on every normal book, and an alarm that fires on the normal case is one
// nobody reads. Their exclusion is already counted, once, as DocumentsExcluded.
unresolved int
// collapsed counts targets that landed in a document another target had already claimed.
//
// ⛔ IT DOWNGRADES THE PROVENANCE, not just the report. A book packing three chapters into one document
// with anchors declares THREE; this package hands back ONE, because it groups whole documents. Calling
// that `declared` would put the engine's own coarser answer out under the format's name — the exact lie
// the spine used to tell. The file did delimit its documents, so `delimited` is the true word for it.
collapsed int
}
// hasProperty reports whether an OPF `properties` attribute carries a token. Token-wise, not substring-wise:
// the attribute is a space-separated list, and `cover-image` must not read as `cover`.
func hasProperty(properties, want string) bool {
for _, tok := range strings.Fields(properties) {
if tok == want {
return true
}
}
return false
}
// hrefTarget resolves an OPF or TOC href against the directory of the file that carried it, returning the
// DOCUMENT it names and whether it pointed at a place INSIDE that document rather than at the whole of it.
//
// ⛔ THE ORDER IS NOT INTERCHANGEABLE, and the two halves of this package used to disagree about it. The '#'
// that opens a fragment exists in the ENCODED form; a literal '#' in a file name arrives as %23. Unescape
// first and that literal becomes a delimiter, truncating the name — which is what the spine resolver did
// while the TOC resolver did the reverse. They are compared against each other, so they parted company on
// exactly the files nobody writes a test for, and the disagreement is silent.
//
// insideDoc is not a detail: a `