v0.9.0
Twenty-four fixers to twenty-eight, plus the widest single extension this
project has made. On the shared shelf — 125 books, epubveri 0.9.14 — errors fall
13,141 → 4,027 over the 28 books epubsana touches, six books reach fully
valid, and both whole-shelf instruments report nothing introduced anywhere.
Two things in here were found by the instruments rather than by reading code, and
both are the reason those instruments exist. The <blockquote> extension came
from a shape census that showed stray text is reported in exactly two containers
on 125 books. The identity fixer's first cut cleared OPF-030 and produced three
new NCX-001s — an edit that was correct while the book got worse, which no
unit test can see.
One was found the other way, by reading epubveri's source: the playOrder
renumbering was target-blind and could have created the mismatch it now
repairs, on a book shape the corpus does not contain.
Added
-
A fixer for an anchor target wrapped around a link
(htm.epub2_dom.nested_anchor). On the shelf every case is one shape — a
footnote reference whose outer<a>carries nohref:<a id="bookmark1"><sup><a href="#footnote1">1</a></sup></a>That outer element is not a link but an anchor target, the legacy way of
naming a position from before every element could hold anid. It is unwrapped
and theidmoves to its single child, so#bookmark1still resolves, to an
element at the same place in the same rendered line.Declines when the outer anchor has an
href(a real link, and which of two
nested links to keep is not ours to decide), when it carries any attribute
besidesid, when the child already has one, or when it wraps more than that
one child. 6 findings in one book, all repaired, nothing introduced. -
A fixer for a package whose declared identifier points at nothing usable
(opf.package.unique_identifier_unresolved+opf.package.opf_identifier_not_empty).
Two rules, one defect at two stages, hitting disjoint sets of five books each:
either no<dc:identifier>carries the idunique-identifiernames, or the one
that does is empty. The declared id is attached to the book's single real
identifier, and any leftover empty element is dropped.It invents nothing. The value was already in the book, written by its
producer; the id was already in the package, written in the attribute. The
repair only attaches one to the other — the same principle asempty_title
moving a TOC label the author wrote.The whole-shelf audit earned its keep here. The first cut cleared
OPF-030
and produced three newNCX-001s: making the package identifier resolvable
is what first lets epubveri compare the NCX'sdtb:uidagainst it, so the
repair unmasked a pre-existing mismatch. No unit test could have caught that —
the edit was correct and the book ended up worse. Thedtb:uidnow syncs in the
same proposal, on the patternfix.manifest_dangling_itemalready set.Measured: 10 findings across 10 books, 3 repaired and 7 declined — and the
declines are the point. Four books carry both a UUID and an ISBN with noidon
either, where which identity is canonical is an editorial decision; two carry no
<dc:identifier>at all, where the repair would have to generate one. With the
NCX syncs the run clears 8 findings and introduces nothing.
Changed
-
fix.ncx_play_ordernow repairs all threeplayOrderfaults, and no longer
risks creating one. epubveri reportsduplicate(different targets sharing a
number),target_mismatch(one target reached by different numbers) andgap
separately, and they interlock — satisfying one naively breaks another. The
fixer now reassigns the whole NCX as the format defines: 1-based, dense, in
document order, with elements naming the same target sharing the first number
that target was given.The previous version numbered by position in the file — unique and dense, but
target-blind, so on a book whose navigation reaches one position by two
routes it would have createdtarget_mismatch. No shelf book had that shape,
so no audit could have shown it; the defect surfaced from reading epubveri's own
rule, which skips a repeated number when all its holders name the same target.
A corpus cannot find that class of defect; the detector's source can.The renumbering now parses the NCX rather than scanning it for
playOrder=, since a target cannot be read off a string — so an NCX that will
not parse is declined rather than rewritten, consistent with the other
structural fixers.Measured: 8
target_mismatchand 1gapcleared on top of the 14
duplicate, nothing introduced, and one more book reaches fully valid. -
The body-level wrapper now also works inside
<blockquote>— the single
largest lever this project has found. XHTML 1.1 requires block content in
<blockquote>exactly as it does in<body>, and the 10 EPUB 2 books added on
2026-08-08 carry 2,508 stray-text and 3,009 incomplete-content findings there.The two messages are one defect seen from opposite ends, which is why both
now trigger the fixer: a<blockquote>holding only text is stray text is not
allowed directly in "blockquote" and element "blockquote" has incomplete
content, because its model needs at least one block child. One wrap clears
whichever fired — verified on real books before a line was written: one file
went{incomplete 53, stray 54, span 1}→{}, another
{incomplete 96, span 98, img 7}→{img 7}, the inline elements clearing as
part of the run.The container set is now a principle rather than a name: the containers
whose XHTML 1.1 model requires block content and admits<div>—bodyand
blockquote.<ol>/<ul>want an<li>and<head>a<title>, wrappers
that assert what the content is, so they stay declined. On the 125-book shelf
that rule is not a compromise but the whole population: stray text is reported
in exactly those two containers and nowhere else.Measured: findings cleared through
schema_violationgo 1,273 →
8,624, total cleared 1,745 → 9,096, books touched 25 → 26, and both
whole-shelf instruments still report nothing introduced anywhere.Honest ceiling: this does not clear all 3,009 incomplete-content findings,
any more than it clears all 403 on<body>(it clears 2 of those). The rest
are containers whose only children are<figure>/<section>— elements XHTML
1.1 does not have — where the repair would be renaming, which epubsana
deliberately does not do. -
Tracks
epubveri0.9.14. Zero source change;styloria0.8 → 0.9 comes
along transitively. Upstream's own figure verified from here: the only
(id, rule)pair that moved isCSS-008 / css.stylesheet.invalid_selector,
0 → 21 in one book, and the shelf run confirms it passes straight through —
errors before 13087 → 13108 and after 3991 → 4012, the same +21 on both
sides, because no fixer of ours consumes it. Everything else identical, zero
regressions.Two upstream items worth recording even though they move nothing here:
RSC-010gained its fallback clause, the defect diagnosed from this side
on 2026-08-08 (a nav/NCX link to a non-Content-Document is legal when the
manifest declares a fallback chain reaching one). No shelf book has the
shape; the IDPFharuko-jpegsample that prompted it loses its three errors.- epubcheck's JSON and XML reports cap identical messages at 25
(CheckMessage.java,MAX_LOCATIONS), with the "N additional locations"
line commented out — so a consumer sees a truncated list with no indication
anything was dropped. epubsana runs no second oracle, so nothing here is
affected, but any cross-tool count taken from epubcheck's JSON is
incomparable above 25 per message. Recorded so it is never quoted by
accident.
-
Tracks
epubveri0.9.12 (from 0.9.9). Zero source change again; the shelf
grew to 125 books the same day. Re-measured: errors 6376 → 4631 over
the 25 books epubsana touches, 5 books reach fully valid, nothing
introduced by either whole-shelf instrument. Every fixer's proposal count is
unchanged, which is the answer to upstream's heads-up: 0.9.12 adds 171
element "img" is missing a required attributefindings across 10 books, and
none of them reaches a fixer of ours. -
handled_rules()now listsncx.uid.package_identifier_mismatchand
ocf.mimetype.not_first_entry. Both sites were rule-less when their fixers
were written, so those fixers dispatch on the bareNCX-001/PKG-006id;
epubveri 0.9.11 named them. The dispatch is unchanged — our floor is 0.9.7,
where the slugs do not exist — but a census reading this list would otherwise
have filed two rules we do fix under "no fixer at all", which is exactly the
mislabelling the list exists to prevent.
Not built, deliberately
<img>with noalt(171 findings / 10 books, the widest-spread shape on
the shelf) is a decline, and upstream predicted it would be. XHTML 1.1 makes
altrequired where HTML5 does not, so this is one of the few places EPUB 2 is
the stricter version. There is no determinate repair:alt=""is correct for a
decorative image and wrong for a meaningful one, and nothing in the finding —
or in the book — says which. Supplying either would be inventing content, which
is the lineempty_titlealready draws. Recorded indocs/COVERAGE.mdrather
than left to be re-derived.
Full notes: CHANGELOG.md. Also on crates.io.