Skip to content

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 18 Jul 13:57
· 25 commits to main since this release

Ten new fixers (nine → nineteen), tracking epubveri through 0.5.15, and the
structural fixers now read the EPUB 2   documents they previously couldn't.
Four whole error families are now handled end to end — entities, doctypes, NCX
internal consistency, and the EPUB 2 <guide>.

Changed

  • Structural fixers now read EPUB 2 content documents that use DTD-only entities
    (&nbsp; under an XHTML 1.1 DOCTYPE). roxmltree doesn't fetch the external DTD, so
    these documents didn't parse and every structural fixer silently declined them —
    the same reach gap epubveri closed in its issue #23. epubsana now declares the
    named entities the document uses in the DOCTYPE's internal subset in a working
    copy, parses that, and maps node byte ranges back to the original text it edits;
    the declarations never appear in the output, and the injection is bounded by the
    DOCTYPE (not a body [1]). Effect on the corpus: fix.empty_title proposals rise
    from 2153 to 2286 (+133) — the previously-unreadable documents that carry a
    title source now get one; the rest parse but are declined for having none. Also
    benefits content_type_meta and bare_text_in_body. Zero new regressions.

Added

  • Two fixers completing the EPUB 2 <guide> family:

    • opf.guide.reference_missing_resource (RSC-007, fix.guide_dangling_reference,
      ConfirmNeeded) — a <guide> reference whose href resolves to no resource in
      the container (on the corpus, a wrong extension like rica.html beside
      rica.xhtml) is dropped: it names a landmark no reader can reach and nothing
      records what file it meant. If dropping leaves the <guide> empty — invalid, and
      the element is optional — the <guide> is dropped too. Matches on the reported
      href; paths are not re-resolved. Dropping the reference also clears the
      co-emitted OPF-031 ("not declared in the manifest").
    • opf.guide.duplicate_reference (RSC-017, fix.guide_duplicate_reference,
      ConfirmNeeded) — two or more references sharing the same type and href;
      the first is kept and the redundant repeats dropped. References with the same
      type but different href are not duplicates and are left alone.

    On the corpus this clears every occurrence in the affected books (missing-resource
    5 books, duplicate 1 book), and the OPF-031 side effect too, with zero
    regressions. This closes the opf.guide family.

  • Two fixers completing the NCX internal-consistency family (RSC-005):

    • ncx.ids.duplicate_id (fix.ncx_duplicate_id, ConfirmNeeded) — two or more NCX
      elements share an id. The first keeps it; each later duplicate is renamed to a
      unique value. NCX ids are not IDREF targets anywhere in an EPUB, so no reference
      is rewritten. Disjoint from the NCName fixer by construction (that one touches
      only ids occurring exactly once; a duplicate occurs more than once).
    • ncx.play_order.duplicate (fix.ncx_play_order, ConfirmNeeded) — navigation
      elements repeat a playOrder. Every playOrder is renumbered to its 1-based
      document-order position (the canonical assignment), making the values unique.
      playOrder is only a hint; the reading order a system follows is the spine,
      untouched.

    With the existing NCName and dtb:uid fixers, this handles the NCX
    internal-consistency family end to end
    : the one remaining member,
    ncx.page_target.invalid_type, is deliberately declined — a bad @type
    (front/normal/special) has no determinate replacement, and guessing the page
    category would be inventing. On the corpus both new fixers clear every occurrence
    in the two affected books (duplicate_id 3→0, play_order 3→0 each), zero
    regressions.

  • Two fixers for an obsolete or unrecognized DOCTYPE (HTM-004), closing out
    the htm.doctype family:

    • htm.doctype.epub3_obsolete_public_id (fix.doctype_html5, AutoSafe) — an
      EPUB 3 document's DOCTYPE carrying a PUBLIC identifier is reduced to HTML5's
      only legal form, <!DOCTYPE html>. Declines a DOCTYPE with an internal subset,
      whose [ … ] declarations HTML5 can't carry.
    • htm.doctype.epub2_unrecognized_public_id (fix.doctype_xhtml11,
      ConfirmNeeded) — an EPUB 2 DOCTYPE whose identifier is a malformed XHTML 1.1
      id (names 1.1 / the xhtml11.dtd but mistypes the exact string) is canonicalized
      to the recognized form. Declines a document declaring a genuinely different DTD
      (XHTML 1.0, a bare <!DOCTYPE html>, OEB): relabeling it to 1.1 would assert a
      content model epubsana can't verify and risks trading the finding for
      content-model errors. On the corpus the one affected book (XHTML 1.0 Strict, 77×)
      is declined, correctly.

    Both are surgical on the DOCTYPE only and bound it by its own closing > (never a
    body [1]), the lesson of the upstream bracket bug. The htm.doctype family is
    now handled end to end — every finding gets a repair or a principled decline —
    though not "every occurrence rewritten"; the decline is the "never guess" rule at
    work. See docs/COVERAGE.md.

  • A fixer for an entity reference missing its closing ;
    (RSC-016 / htm.entity.missing_semicolon, fatal) — &nbsp for &nbsp;.
    A recognized name is replaced by the character it denotes (well-formed with or
    without a DTD); one of the XML-predefined five (&amp …), whose character is
    the bare delimiter itself, is closed with the missing ; instead. The match is
    boundary-checked, so a correct &nbsp; and a longer entity that merely starts
    with the name are never touched; an unrecognized name is declined.

    This completes the htm.entity family — the first error family epubsana
    covers end to end: every entity defect epubveri reports (undeclared, and now
    unterminated) has a repair. See docs/COVERAGE.md for the family map this
    closure is measured against.

  • Two fixers for dangling references in the package document, contributed as
    requirements by epublift, which carried its own repair for them
    (#4,
    #3):

    • RSC-001 / opf.manifest_item.missing_resource — a manifest <item>
      declaring a resource the container doesn't hold. The declaration is dropped
      together with every reference that named it: the spine <itemref>s it
      would otherwise orphan, and a legacy <meta name="cover"> pointing at it.
      Those travel in a single proposal rather than separate ones, because
      approving the item drop and declining the spine drop would leave you with an
      OPF-049 epubsana created itself.
    • OPF-049 / opf.spine.itemref_idref_not_in_manifest — a spine <itemref>
      naming a manifest id that does not exist. Dropped; every other entry keeps
      its place in the reading order.

    Both are ConfirmNeeded — they are deletions that can shorten the reading
    order or remove a cover declaration, and epubsana does not delete visible
    structure unattended. Both decline rather than repair when the deletions would
    leave <spine> with no children: a spine-less EPUB is not a repaired book.

    On the 171-book corpus this clears every RSC-001 (3 findings in 2 books) and
    takes both books from invalid to fully valid — 26 → 28 books that epubsana
    brings all the way to valid. No book gains a finding.

  • A fixer for a duplicated spine entry — the same manifest item listed twice,
    so a chapter appears twice in the reading order
    (#2). The first occurrence
    is kept and the repeats dropped: the repeat carries no information the first
    doesn't, and the first is where the document belongs in the sequence.

    epubveri reports this condition under two idsOPF-034 in EPUB 2,
    RSC-005 in EPUB 3 — with one shared rule, so the fixer keys on the rule
    and inherits the id from the finding. A fixer written against OPF-034 alone
    would have done nothing on every EPUB 3 book.

    Declines rather than guessing when the duplicate's linear disagrees with the
    first's (the book means "in the reading order and reachable out-of-line",
    which is deliberate), or when a repeat carries an id that a <meta refines>
    targets. Not present in the reference corpus, which contains no Kindle→EPUB
    conversions — it lands on epublift's reproduction of it in the wild.

Changed

  • Track epubveri 0.5.15 (from 0.5.9). No source change across the whole span
    — the rule/params contract held every bump; the effects are behavioural.

    • epubveri#23 (0.5.12): EPUB 2
      documents with DTD-declared entities (&nbsp; under an XHTML 1.1 DOCTYPE) now
      parse, so a class of false RSC-012 "fragment not defined" findings is gone.
      RSC-012 drops from 1247 to 172 — the 172 are the genuinely dangling fragments,
      the ~1075 removed were the detector failing to read a valid document and calling
      its ids absent. empty_title findings rise +157 as the same documents become
      readable.
    • epubveri#25 (0.5.12): a
      regression in 0.5.10/0.5.11 that turned any EPUB 2 document with a [ in its
      body (a footnote marker) into a false fatal — 78 across 11 corpus books on
      0.5.11, zero on 0.5.12.
    • Content-model validation (0.5.13 EPUB 2, 0.5.15 EPUB 3): EPUB 2 books are
      now checked against XHTML 1.1 + OPS 2.0.1 instead of HTML5, and EPUB 3 nesting /
      IDREF rules are enforced. This is verdict-changing: opf.content_document.schema_violation
      rises from 16 to 32 books, and new RSC-005 sub-codes appear
      (htm.epub2_dom.nested_anchor, htm.epub2_dom.html5_only_element). epubsana
      does not yet repair these classes.

    Net on the 171-book corpus: 25 books brought all the way to valid (was 30 on
    0.5.12), still zero regressions. The drop is not lost coverage — every fixer
    fires identically; it is the content model finding real defects epubsana does not
    yet fix. Measured: 14 books are now blocked from fully-valid only by a new
    content-model finding
    , which is the ROI case for a content-model fixer.

    The manifest floor is now epubveri = "0.5.15" — earlier versions either misreport
    (RSC-012 on 0.5.9, the #25 fatal on 0.5.10/0.5.11) or predate content-model parity.

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