Skip to content

v0.13.1

Choose a tag to compare

@github-actions github-actions released this 29 Aug 00:11
· 18 commits to main since this release

Seven false positives, and none of them was reaching anyone. Every one is a
parity fix against epubcheck's own fixtures: measured across the 415-book local
shelf, not one of the seven changes a real book. What the day actually adds to
a reader's output is three true findings on two books, each confirmed against
epubcheck at the same file, line and wording.

The content-document set is the version's set, and ours was version-blind
(issue #129). OPS 2.0.1's content documents are XHTML and DTBook; EPUB 3's are
XHTML and SVG — so the two differ in both directions, and a predicate that
ignores the version is wrong twice. Four sites consulted it:

  • a hyperlink to a manifest-declared SVG that is not in the spine drew
    RSC-011 from us and RSC-010 from epubcheck at 2.0 — a wrong id on entirely
    ordinary markup;
  • a <guide> reference to an SVG is OPF-032 at 2.0 and we were silent,
    the EPUB 3 mirror (a guide reference to DTBook) likewise;
  • the NCX <content src> needed both of epubcheck's questions rather
    than one. It now shares hyperlink_abort instead of restating its
    conditions, which is how it had come to hold the first arm and not the
    second: an NCX pointing at a declared XHTML document that is simply not in
    the spine
    drew nothing at all from us;
  • the fallback chain, the hyperlink's escape hatch from RSC-010, was the
    last and the sharpest. FallbackChainResolver calls
    isBlessedItemType(type, version), so a PDF that falls back to an SVG is
    rescued at 3.0 and not at 2.0, DTBook being the mirror. Half of those four
    cells were a wrong id, and the comment at that site had claimed the resolver
    applies "no version condition" — it never did.

The other three sites are not version-dependent, read at the source rather
than probed: ResourceReferencesChecker:179 gates fragment resolution on
MIMEType.SVG.is(..) || MIMEType.XHTML.is(..) with no version in the call. One
dead line went with them — the <guide> site carried the same content-document
guard twice in a row, the second unreachable.

A data: URL in a hyperlink is not finished at RSC-029 (issue #128).
epubcheck does not stop there: the reference reaches the ordinary hyperlink
checks and is asked the same two questions of the media type the data URL
declares for itself. Fourteen books later — one per type per version — the
either/or is gone: at 3.0 we reported RSC-029 alone where epubcheck reports it
and RSC-010 or RSC-011; at 2.0 we reported RSC-010 for every type, right for
an image or a PDF and wrong for text/html, XHTML and DTBook. The comparison
is case-sensitive, so data:TEXT/HTML,x and data:text/html,x differ.

OPF-030 was asked only inside the <metadata> block, and OPF-048 could not
be reached at all.
Resolving unique-identifier lived under
if let Some(md) = metadata, so a package with no <metadata> element drew no
OPF-030 — and one with neither the element nor the attribute drew nothing:
not the RSC-005, not OPF-048, not OPF-030. epubcheck reports all three. No
fixture anywhere builds that book, which is why the regression test does.

A <package> in a foreign namespace now stops after the schema error. A
namespace that is neither the OPF one nor a legacy one means the document is
not a package document, and epubcheck's grammar says so by rejecting the root —
after which it never builds the package model, asks nothing about the manifest,
and opens no content document. We kept going: two RSC-001 on its own fixture,
and on a book carrying a genuinely broken content document, two more RSC-005
from validating a document epubcheck never opens. What is left behind is a
strict subset of epubcheck's findings, and the stop is guarded on the schema
violation having actually been reported rather than on the namespace test
alone.

OPF-097's three exemptions were all wider or narrower than epubcheck's.
isNcx() is set in exactly one place — on the item the spine's toc attribute
resolves to — so an NCX-typed item that no toc names is not exempt, and our
media-type test excused it. isInSpine() is a property of the item, not of
the resource: two manifest items may declare the same href, and then only the
one the spine names is exempt. And a data: href was not asked the question at
all. A data URL is named by its first 30 characters plus an ellipsis, as
epubcheck names it; the first version put three kilobytes of base64 into a
usage message.

A remote <base> restricts the stylesheet and not the hyperlink. RSC-006
says a remote reference is not allowed in this context, and a hyperlink is
not one of those contexts — you may link to a website. We had it inverted:
RSC-006 on every relative <a href> in such a document, and silence on the
<link rel="stylesheet"> beside it. The corpus could not see this, because
both of its base fixtures expect one RSC-006 and got one, from the wrong
element. The restricted-remote classification is now one predicate shared by
the two sites that ask it, which is how they had drifted: only a reference
written remote was asked, never one that resolves remote through a base.

A leaking URL that the manifest declares and a content document references
was two findings.
It is one fault, and epubcheck reports it once — the last
row on the W3C conformance suite where our count exceeded theirs, and it had
been there throughout. The rule is narrower than "once per URL", which is what
the third arrangement pins:

epubcheck before
declared only 1, at the manifest 1
declared and referenced 1 2
the same URL in two content documents, undeclared 2 2

So a declaration and a reference to that declaration are one fault, while two
independent references are two. The first version of the suppression deduped
across content documents as well and broke the third case.

The manifest finding also moved from push_at_pos to push_full, so its href
reaches params: a finding whose value lives only in its message text cannot
be queried structurally, and the content-document pass has to ask whether the
URL was already reported rather than trust a claim about what the manifest pass
did. Our surviving finding sits at the manifest and epubcheck's at the content
document — the position policy below, not an accident.

SVG in EPUB 2 is validated normatively (issue #93)

schema/20/rng/content.rng includes the SVG 1.1 modules directly, so inline
and standalone SVG in an EPUB 2 publication is validated normatively and
reported as ERROR RSC-005, where EPUB 3 runs the same grammar informatively as
RSC-025 usage. Three checks were gated to EPUB 3 on a comment that is correct
about RSC-025 and wrong about the validation underneath it — the gate was not
avoiding an opinion epubcheck lacks, it was suppressing a finding epubcheck
makes. Its own example proves it: a lowercase viewbox in a real book had been
written off there as our false positive, and epubcheck reports it.

Both lists were diffed against the authority before either arm was switched on,
and nothing was missing: 0 of the 81 element names and 0 of the 256
unprefixed attribute names
schema/20/rng/svg/*.rng declares are absent from
ours. What ours carry beyond SVG 1.1 is one element (feDropShadow) and four
attributes (focusable, href, rel, tabindex), each probed in both
versions: all are RSC-005 at 2.0 and clean at 3.0.

The content model now covers its closed half, in four shapes measured cell
by cell — 33 books, one per cell:

  • the graphics elements (rect, circle, image, use, path, tref, …)
    hold descriptive and animation elements and nothing else, not even text;
  • the text elements (text, tspan, textPath) are a mixed pool that also
    admits a, altGlyph, tref and tspan — and textPath only directly
    inside <text>;
  • the gradients add <stop>;
  • <stop> itself takes animation elements only, not even a <desc>.

Indentation whitespace is not loose text, which is the one cell of the 33 that
could have cost a real book. The container elements — g, defs, svg, a,
switch, marker and the rest — are deliberately outside this slice: their
models are open-ended pools, and that is where a from-scratch grammar starts
reporting what epubcheck does not.

Measured cost before and after: of the shelf's 261 EPUB 2 books carrying inline
SVG, the whole population uses two element names and nine unprefixed
attribute names. Three of the nine are outside SVG 1.1 — one occurrence each,
two books — and all three are errors epubcheck reports at the same line.

Two more parity fixes, both settled by reading epubcheck's source

An EPUB 3 spine toc must resolve to an NCX (issue #127). package-30.sch's
opf.toc.ncx asserts from opf:spine[@toc] that the referenced item is an
NCX, and it fires whenever a toc attribute is present and does not resolve to
one — the unresolved case included, since an absent media type fails the assert
too. We reported the OPF id alone. EPUB 3 only; at 2.0 epubcheck reports the
OPF id alone as well.

That issue had been filed on the belief that our RSC-001 was standing in for
epubcheck's CHK-008. OPFChecker:381-400 says otherwise: CHK-008 is the
catch (IllegalStateException) around checker.check(), i.e. epubcheck
announcing that one of its own checkers threw. toc.ncx() is called on
whatever the spine's toc resolves to, unconditionally, so a toc naming an
XHTML item makes it build an NCXChecker over a non-NCX context — which throws
before the document is ever opened. Handed such a book with a deliberate error
inside that document, epubcheck reports nothing about it. Our extra findings
there are true findings it loses to its own exception handling.

CHK-008 therefore stays N/A, and COVERAGE.md now says why in its own words:
the family note had been corrected in August while the table row still
carried the corrected-away reason, because an id with no ANN entry inherits
the family's.

The package's identity survives a fatal parse error (issue #126). epubcheck
reads as a stream, so when a package document falls apart it has already passed
the root start tag, and everything depending on nothing else still runs there —
RSC-005 and OPF-048 for a missing unique-identifier, OPF-030 for one that
cannot resolve. Measured: three findings, not the one the issue assumed. We had
no tree and said nothing.

The root start tag is now recovered lexically — prolog, comments, processing
instructions and a DOCTYPE with an internal subset skipped — and handed back to
roxmltree as a self-closing document, so no XML rule is re-implemented. The two
shapes that break a naive scan have tests: a > inside a quoted attribute
value, and a > inside a DOCTYPE's internal subset. Near-zero risk by
construction: it runs only where we previously reported nothing beyond the
fatal, on a book that is INVALID either way.

The SVG content model, in four increments and 146 measured cells

Every rule below was measured against 5.3.0 one book per cell, and every cell
is a test. Ten shapes:

  • the graphics elements (rect, circle, image, use, path, tref,
    …) hold descriptive and animation elements and nothing else, not even text —
    though indentation whitespace is not text, which is the one cell of the first
    increment that could have cost a real book;
  • the text elements (text, tspan, textPath) are a mixed pool that also
    admits a, altGlyph, tref and tspan, with textPath allowed only
    directly inside <text>. tref is not one of them: it names the text it
    renders, so its own model is closed;
  • the gradients add <stop>, and <stop> itself takes animation
    elements only — not even a <desc>;
  • clipPath takes the shape elements plus use, and not every graphics
    element: <g> and <image> are errors inside one;
  • filter takes the primitives, and its three sub-elements (feMerge,
    feComponentTransfer, the two lighting primitives) are stricter than
    anything else here — neither descriptive nor animation, only their own
    children;
  • the containers (svg, g, defs, a, switch, marker, symbol,
    pattern, mask) were the increment that corrected an earlier assumption.
    They are not open-ended pools: a container holds every SVG element that does
    not belong to a specific parent, and no character data. Stated as an
    exclusion rather than a 39-name allow-list, and verified in both directions —
    all 41 excluded names rejected inside a <g>, all 39 remaining ones
    accepted. <a> is the one container that also carries character data, in
    both of its contexts.

Cardinality is two rules rather than a family. Of everything that could
require a child, only the lighting primitives do — an empty feMerge,
feComponentTransfer, clipPath, filter or gradient is clean. They take
exactly one light source; a second is "not allowed here", which is how
epubcheck words it too. And feComponentTransfer's children are the ordered
sequence feFuncR?, feFuncG?, feFuncB?, feFuncA?, each at most once.

Two of those rules nearly shipped wrong, and both were caught by measuring the
cell rather than reasoning about it. The feFunc names had been listed
alphabetically — harmless while the model was a set membership test, and an
inversion the moment it became a sequence. And <a> was first given the plain
container model, which an assertion written for the text family a commit
earlier caught. The 209-publication W3C suite would not have: it contains no
<a> carrying text inside SVG, and reported no disagreement on that build.

Nothing here moves a book on the 415-book local shelf.

The required-attribute table went from seven elements to twenty-six, plus
six whose required attribute is the namespaced xlink:hrefuse, feImage,
mpath, textPath, tref and cursor. Twenty-five cells, one book each.

Those six are the part worth knowing. has_attr_no_ns cannot see a namespaced
attribute at all, so they could not have been in the main table — and the
grammar extraction that generated the candidates for everything else missed
every one of them too, because the xlink attributes are declared in their own
module rather than in each element's attlist. The extractor was a candidate
generator, not an authority
; every row is a measured book. animateMotion,
pattern and marker were probed and require nothing, and are named in the
code so the next reader does not re-probe them.

This axis surfaced as a by-product: the container probes used deliberately bare
elements, and epubcheck kept reporting a second finding the containment
question had nothing to do with.

The datatype axis barely exists, and finding that out is most of what it
took.
schema/20/rng/svg/svg-datatypes.rng declares 22 datatypes and 17
are a plain <data type="string"/>
SVGLength, Number, OpacityValue,
TransformList, PathData and SVGURI carry their meaning in an
<a:documentation> and constrain nothing. Probed rather than inferred, against
a control confirming the document is validated at all: width="abc",
width="-5", r="-1", opacity="junk", transform="notafunction(1)" and an
invalid path d are every one of them clean in epubcheck. Constraining them
would be inventing errors, which is the restrictive direction ADV-* exists
to keep out of the verdict.

Five attributes are genuinely constrained and are now checked: clip-rule and
fill-rule (nonzero | evenodd | inherit), externalResourcesRequired and
preserveAlpha (a boolean), and preserveAspectRatio (ten alignment keywords,
optionally meet or slice — a regular expression in the grammar and a
hand-written matcher here rather than a new dependency).

This is the first SVG increment whose silence on the shelf means something:
260 of its EPUB 2 books carry preserveAspectRatio, 309 occurrences, three
distinct values
, and all three are valid. The check runs over a real
population and finds nothing.

With that, every axis this issue named is closed — the element vocabulary, the
attribute vocabulary, the content models, the required attributes and the
datatypes.

Positions are unchanged, and that is a decision

An empty <guide> draws the same finding from both tools at different
coordinates: epubcheck at the character after </guide>, epubveri at the
<guide> itself. Both are internally consistent. epubcheck reports where its
reader had reached when the fault surfaced, which gives it three different
anchors for three kinds of fault — just past the start tag, just past the end
tag, and just past the start tag's > for an attribute, where it points at
neither the element nor the attribute. We report where the fault begins,
because that is where an author has to go and where a repair tool has to start.
range().end reproduces epubcheck's number exactly if this is ever revisited;
the reasoning now lives on Position::of.

Full notes: CHANGELOG.md. Also on crates.io.