bindery-cli 0.46.1__tar.gz → 0.47.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/CLAUDE.md +20 -5
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/PKG-INFO +8 -3
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/README.md +7 -2
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/patchnotes.md +101 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/pyproject.toml +1 -1
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/roadmap.md +1 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/spec.md +54 -7
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/src/bindery/__init__.py +1 -1
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/src/bindery/audit.py +37 -2
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/src/bindery/cli.py +464 -232
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/src/bindery/epub.py +130 -10
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/src/bindery/transforms.py +246 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_audit.py +97 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_cli.py +82 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_epub.py +50 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_flags_real_core.py +76 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_flags_wiring.py +5 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_transforms.py +234 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_version.py +35 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/uv.lock +2 -2
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/.github/FUNDING.yml +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/.github/workflows/ci.yml +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/.github/workflows/publish.yml +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/.gitignore +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/AGENTS.md +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/LICENSE +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/docs/screenshots/library-sweep.png +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/logo.svg +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/plugin/__init__.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/project.done +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/run_tests.sh +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/scripts/FastSweep.java +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/scripts/build_plugin.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/scripts/fast_sweep.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/src/bindery/__main__.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/src/bindery/library.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/src/bindery/pagination.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/src/bindery/reserialize.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/src/bindery/validate.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/src/bindery/watermark.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_comment_hyphens.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_cover_tocdrift.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_library.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_matcher_hardening.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_pagination.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_plugin.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_reserialize.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_space_renames.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_validate.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_watermark.py +0 -0
- {bindery_cli-0.46.1 → bindery_cli-0.47.1}/tests/test_xml_hardening.py +0 -0
|
@@ -30,15 +30,27 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
|
|
|
30
30
|
`xmlns`, bare `&`, named entities, void self-closing), the NCX pipeline, and the
|
|
31
31
|
mimetype fix (added/normalized/first-stored, epub.py's archive rewrite); every core
|
|
32
32
|
fix must render identically to the author's intent: never add, remove, or reorder
|
|
33
|
-
visible content. The canonical opt-in inventory lives in spec.md:
|
|
33
|
+
visible content. The canonical opt-in inventory lives in spec.md: twenty-one structural
|
|
34
34
|
repairs, four lossy strips, four safe opt-ins, plus `--reserialize`.
|
|
35
35
|
* **Structural repairs** (`--fix-empty-body`, `--fix-missing-title`, `--fix-id-colons`,
|
|
36
36
|
`--fix-page-map`, `--strip-epub3-attrs`, `--downgrade-epub3-tags`,
|
|
37
37
|
`--unwrap-block-in-inline`, `--strip-invalid-value`, `--unwrap-illegal-tags`,
|
|
38
38
|
`--prune-missing-resources`, `--strip-broken-anchors`, `--encode-url-spaces`,
|
|
39
39
|
`--fix-container`, `--fix-media-types`, `--fix-cover`,
|
|
40
|
-
`--fix-comment-double-hyphen
|
|
41
|
-
|
|
40
|
+
`--fix-comment-double-hyphen`, `--fix-svg-dup-ids`, `--fix-cdata-terminator`,
|
|
41
|
+
`--fix-misnested-inline`, `--fix-stray-close`, `--fix-unterminated-attr`;
|
|
42
|
+
transforms.py, threaded through epub.py). The v0.47.0 five (the 2026-10-06
|
|
43
|
+
issue batch): `--fix-svg-dup-ids` renames duplicate glyph ids inside
|
|
44
|
+
standalone `.svg` entries (first keeps its name, references resolve to the
|
|
45
|
+
first definition so they are never rewritten); `--fix-cdata-terminator`
|
|
46
|
+
completes a truncated `/*]]>` CDATA-terminator comment inside inline
|
|
47
|
+
`<style>` blocks (CSS-008); `--fix-misnested-inline` normalizes the
|
|
48
|
+
Mobipocket drop-cap mis-nest `<i><b>X</i></b>` (punctuation between the
|
|
49
|
+
reversed closers survives outside both spans; a letter run is refused);
|
|
50
|
+
`--fix-stray-close` removes an end tag with no open start frame (a merely
|
|
51
|
+
mis-nested pair is never touched); `--fix-unterminated-attr` closes an
|
|
52
|
+
attribute value open to the tag's own `>` (bare-word values, no quote
|
|
53
|
+
before the next `<`); all normal `gate`. `--fix-comment-double-hyphen` (v0.46.0, the RSC-016 fatal)
|
|
42
54
|
replaces `--` inside XML comments with an en-dash: comment bodies only (text
|
|
43
55
|
nodes and CDATA never touched, the one deliberate exception to the
|
|
44
56
|
never-rewrite-comments invariant), in content documents, the NCX, and the OPF
|
|
@@ -71,7 +83,10 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
|
|
|
71
83
|
in `RepairReport.watermark_refusals`, and surfaced as a `manual_watermark_repair`
|
|
72
84
|
decision by `run phase1` on both the read-only and apply paths. `--strip-stub-docs`
|
|
73
85
|
(2026-09-15) mirrors emptytext's placeholder signals: identical short text across
|
|
74
|
-
>= 3 spine docs and >= 30% of the spine; the
|
|
86
|
+
>= 3 spine docs and >= 30% of the spine; since v0.47.0 the pool is the `<body>`
|
|
87
|
+
span and image-carrier pages (empty body text, an `<img>` present) are exempt, so
|
|
88
|
+
identical head `<title>`s can never weld illustrated divider pages into a fake
|
|
89
|
+
stub class (the Great Change misfire); the whole-spine-stubs book is refused
|
|
75
90
|
(EMPTY, re-source), and the drop cascades to archive entries, manifest, spine,
|
|
76
91
|
NCX navPoints, and nav toc entries.
|
|
77
92
|
Do not let any NEW fix touch content without its own flag; if a candidate repair cannot
|
|
@@ -103,7 +118,7 @@ Born from the 2026 library audit (see the user memory `calibre-library-epubcheck
|
|
|
103
118
|
- `src/bindery/epub.py`: archive rewrite, NCX uid sync, RepairReport, mismatch detection, and the opt-in structural-repair plumbing (including the CSS precondition scan). Since v0.46.0 the opt-in selection is the `RepairFlags` dataclass (one field per flag, all off by default; the all-off instance IS the default pass): `repair_epub(src, dst, flags)` and `process_book(..., flags)` take one object instead of a ~25-kwarg signature, the EPUB2-targeted fixes' version gate swaps in a `dataclasses.replace` copy (never mutating the caller's instance), and the plugin's bare `repair_epub(src, dst)` call constructs exactly the all-off default pass. Since v0.38.0/v0.39.0 (Phase 16): `generate_container` + the `--fix-container` gateway (insert after mimetype when missing, replace in-loop when stale; constant-epoch timestamp keeps bytes deterministic), `prune_dangling_edges` (prune_missing_manifest_items returns the pruned ids and the edges that pointed at them (spine@toc, media-overlay, fallback, EPUB2 cover meta) are rewritten so pruning cannot manufacture the regression the gate rejects), and `fix_manifest_media_types` (extension map + magic-byte confirmation via a `peek` callable over the open source zip; jpg/png/gif only), and `fix_cover_meta` (the ruling's deterministic half: dangling EPUB2 cover meta re-pointed from the guide's cover reference when exactly one manifest item carries that file, removed otherwise; accepted under no_worse like the lossy strips because cover wiring is invisible to epubcheck).
|
|
104
119
|
- `src/bindery/validate.py`: epubcheck wrapper, the `gate` (improvement) and `no_worse` (no-regression, for the lossy strips) acceptance bars.
|
|
105
120
|
- `src/bindery/library.py`: Calibre walk, atomic replace, backups, and native format installation. Since v0.19.0 `CalibreIdResolver` resolves the book id from `metadata.db` through cquarry (one lazy path→id map per run; since v0.20.0 the map comes from `CalibreDB.format_path_index()`, re-normalized with `resolve().lower()` for the resolver's symlinked-directory and case-insensitive matching); the `(id)` directory-name regex is only the no-catalog fallback. Since v0.24.0 `install_format()` places the repaired file atomically and updates the `data` row through `cquarry.write.WritableCalibreDB` (since v0.31.0 via `set_format`, cquarry 1.17's sanctioned remove+add in one transaction); the external `calibredb` CLI is gone. Since v0.32.0 a guessed id drives a row update only when metadata.db corroborates it (`_verify_guess`: books row exists, file in the book's own directory, catalogued `data.name` when a row exists); anything else saves in place and leaves the catalog untouched: never reintroduce directory-name guessing as the primary source: renamed/mismatched directories would replace the wrong book. Since 2026-09-15 `make_backup` takes the opt-in `--backup-keep N` ring: at most N backup files per book, the author original `.bak` never deleted, newest content at the highest name; default remains an unbounded rotation.
|
|
106
|
-
- `src/bindery/cli.py`: `repair` and `library` subcommands, including `--all` and `--install-to-calibre`; `repair --json` emits the library per-book record vocabulary (2026-09-15; since v0.45.0 every per-book record, the phase1 payload included, also carries the structured fix breakdown: the `fixes` dict plus `ncx_uid_synced`/`watermark_refusals` off the `Outcome`, and `_phase1_decisions` reads that data instead of substring-matching the rendered `summary`, which stays for human eyes; CalibreQuarry's lossy-consent mirror is the waiting consumer, floor `bindery>=0.45.0`); since v0.29.0 also the `run` verbs: `phase1` (pre-import vetting over loose files: the audit battery composed with one gated repair sweep; read-only until `--apply-lossy`, which IS the lossy-strip consent) and `phase3` (the scoped post-import apply step: `library --id --sweep --only all --apply --all --install-to-calibre` plus a pre/post summary; mechanically refuses unscoped sweeps with exit 2; since 2026-09-15 the summary sums applied/equal states only and carries rejected projections as `rejected_projection`). Plus `bindery doctor` (2026-09-15): the environment self-check that imports none of the VirInvictus stack, never raises, and always exits 0. Both run verbs take `--non-interactive` and surface open questions as `decisions_needed` in their JSON, never prompts. They drive the shipped `run_library` through the real parser, so no flag can drift between the verb and the subcommand it wraps. Since v0.46.0 `cli._flags_from_args(args)` is the one place the argparse names map onto `RepairFlags` fields (each `or --all`), shared by `run_library` and `run_repair`; the old per-verb ~25-kwarg blocks are gone. `library --workers N` (v0.30.0) parallelizes only the sweep's candidate pass, in windows of N consumed in order so `--limit` stays lazy; the repair phase is never parallel (shared workdir, atomic-replacement contract). Since v0.40.0 `main()` guards the VirInvictus stack: a `ModuleNotFoundError` naming `vir_tui`/`cquarry` (marker-gated to 3.14+ on the 3.12 floor) becomes one stderr line and trouble exit 2 instead of a traceback; anything else missing still raises.
|
|
121
|
+
- `src/bindery/cli.py`: `repair` and `library` subcommands, including `--all` and `--install-to-calibre`; `repair --json` emits the library per-book record vocabulary (2026-09-15; since v0.45.0 every per-book record, the phase1 payload included, also carries the structured fix breakdown: the `fixes` dict plus `ncx_uid_synced`/`watermark_refusals` off the `Outcome`, and `_phase1_decisions` reads that data instead of substring-matching the rendered `summary`, which stays for human eyes; CalibreQuarry's lossy-consent mirror is the waiting consumer, floor `bindery>=0.45.0`); since v0.29.0 also the `run` verbs: `phase1` (pre-import vetting over loose files: the audit battery composed with one gated repair sweep; read-only until `--apply-lossy`, which IS the lossy-strip consent) and `phase3` (the scoped post-import apply step: `library --id --sweep --only all --apply --all --install-to-calibre` plus a pre/post summary; mechanically refuses unscoped sweeps with exit 2; since 2026-09-15 the summary sums applied/equal states only and carries rejected projections as `rejected_projection`). Plus `bindery doctor` (2026-09-15): the environment self-check that imports none of the VirInvictus stack, never raises, and always exits 0. Both run verbs take `--non-interactive` and surface open questions as `decisions_needed` in their JSON, never prompts. They drive the shipped `run_library` through the real parser, so no flag can drift between the verb and the subcommand it wraps. Since v0.46.0 `cli._flags_from_args(args)` is the one place the argparse names map onto `RepairFlags` fields (each `or --all`), shared by `run_library` and `run_repair`; the old per-verb ~25-kwarg blocks are gone. Since v0.47.1 the help is two-level and the repair-flag inventory is one table: `_REPAIR_FLAG_TABLE` (dest + one-line short help + long reference text) registers the compact grouped `--help` (argparse argument groups, hand-short usage line) and `--help-repairs` renders the long descriptions (a custom argparse Action, deliberately NOT store_true so the wiring test's selection count ignores it); adding a flag means adding a table row. `library --workers N` (v0.30.0) parallelizes only the sweep's candidate pass, in windows of N consumed in order so `--limit` stays lazy; the repair phase is never parallel (shared workdir, atomic-replacement contract). Since v0.40.0 `main()` guards the VirInvictus stack: a `ModuleNotFoundError` naming `vir_tui`/`cquarry` (marker-gated to 3.14+ on the 3.12 floor) becomes one stderr line and trouble exit 2 instead of a traceback; anything else missing still raises.
|
|
107
122
|
- `plugin/__init__.py` + `scripts/build_plugin.py`: the Bindery Repair Calibre plugin (v0.37.0; identity `Bindery Repair` / `bindery_repair`, approved and built per the scoped spec in roadmap.md Phase 3). The builder vendors `transforms.py`, `epub.py`, `pagination.py`, `watermark.py`, `reserialize.py` byte-identically into the zip (the zip root is a package, so their relative imports resolve unchanged; the suite's drift test pins the equality) with the version tuple substituted from the single-source VERSION; `publish.yml` attaches `BinderyRepair-v<VERSION>.zip` to each release. The plugin runs ONLY the default pass (epubcheck cannot gate inside Calibre), never raises, is byte-idempotent (zero fixes returns the original path), testzip()-verifies its output, logs one line per book, and takes JSON config via `site_customization` (`log`, `log_path`, `max_size_mb` default 150, `max_log_mb` rotation cap default 2, `epubcheck_path` experimental validation default OFF). Load it in tests through the `tests/test_plugin.py` stubs: the calibre namespace is installed in sys.modules and the package loads with a two-dir `__path__` (plugin dir first, then src/bindery) exactly like calibre's zipplugin loader shape. **Plugin/interpreter compatibility (v0.40.0):** v0.39.0 shipped PEP 758 unparenthesized except tuples, and the plugin died with a bare SyntaxError on every released Calibre (7.x and the whole 8.x series embed Python 3.11, 9.0+ embeds 3.14; from calibre's bypy sources.json, checked against the upstream clone). The core is now grammar-pinned: parenthesized except tuples everywhere, `ruff`'s `target-version = "py312"` pin (inference from requires-python would rewrite the parens back off), `tests/test_version.py`'s parse guard, and CI's `plugin-compat` job byte-compiling the built zip under 3.11/3.12/3.13/3.14. `minimum_calibre_version` is (7, 0, 0): the oldest series whose interpreter CI covers, replacing the hollow (2, 0, 0); `supported_platforms` stays `["linux"]` (developed and tested on Linux only). Any new syntax in the vendored modules must clear the guard before it ships.
|
|
108
123
|
- `tests/`: transforms, end-to-end repair, atomic replace, pagination, watermarks, the audit analyzers (content/pagenumbers/emptytext/ocr/monolithic/completeness, archive integrity, spine classification), the library sweep and `CalibreIdResolver`, validate, reserialize, CLI wiring, the version pin and the syntax-floor grammar guard, and the Calibre plugin (build drift + run behavior under calibre stubs). Since the v0.46.0 follow-ups: `tests/test_flags_wiring.py` reflectively pins the RepairFlags inventory to the argparse dests and `_flags_from_args` mapping (a flag added without a field, a field without a flag, or a remapped line fails there), the all-off defaults, and the version gate's caller-immutability contract (test_epub's `TestVersionGateKeepsFlagsIntact`); `tests/test_comment_hyphens.py` carries the RSC-016 fix; `tests/test_flags_real_core.py` drives every repair flag from argv through the unmocked core (--no-validate; the gate itself stays pinned by the mocked-oracle tests and runs for real wherever epubcheck exists). All three ride CI's core-compat 3.12/3.13 leg; the hand-listed module list there must gain an entry for every new stack-free test module.
|
|
109
124
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bindery-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.47.1
|
|
4
4
|
Summary: Audit EPUB body text and repair broken EPUBs with deterministic, epubcheck-gated fixes; atomic in-place replacement in a Calibre library.
|
|
5
5
|
Project-URL: Homepage, https://github.com/VirInvictus/bindery-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/VirInvictus/bindery-cli
|
|
@@ -51,7 +51,7 @@ bindery-cli makes accidentally broken markup well-formed again. It does not rewr
|
|
|
51
51
|
|
|
52
52
|
Everything below is off until its flag is passed (or all at once via `--all`). They go further than well-formedness (altering structure or adding minimal content), so they never run by default:
|
|
53
53
|
|
|
54
|
-
- **`--fix-ids`**: rewrite ids that are not valid XML names (start with a digit, contain a colon) in the OPF manifest, updating every reference to them (spine, fallback, media-overlay, the EPUB 2 cover meta), and in the NCX (where old conversions stamp navPoint ids from UUIDs). Touches the OPF, so it is off by default; the dc: metadata is never altered.
|
|
54
|
+
- **`--fix-ids`**: rewrite ids that are not valid XML names (start with a digit, contain a colon, or carry any character outside the XML NameChar set, like the apostrophes calibre copies from filenames) in the OPF manifest, updating every reference to them (spine, fallback, media-overlay, the EPUB 2 cover meta), and in the NCX (where old conversions stamp navPoint ids from UUIDs). Touches the OPF, so it is off by default; the dc: metadata is never altered.
|
|
55
55
|
- **`--add-img-alt`**: add `alt=""` to `<img>` elements missing the required attribute. Renders identically, but it adds markup the author never wrote, and an empty alt tells a screen reader the image is decorative; hence opt-in.
|
|
56
56
|
- **`--reserialize`**: rebuild content documents that are still malformed by re-parsing them with html5lib and re-emitting XHTML, closing unclosed `<p>`/`<div>`/`<span>`/`<blockquote>` that the regex transforms cannot. Runs only on documents that are not already well-formed, so good files are untouched.
|
|
57
57
|
- **`--strip-bad-attrs`**: drop attributes that are invalid XML (a name starting with a digit, or a namespaced name whose prefix is never declared, like Office VML `v:shapes`). Surgical and a no-op on well-formed files.
|
|
@@ -72,6 +72,11 @@ Everything below is off until its flag is passed (or all at once via `--all`). T
|
|
|
72
72
|
- **`--fix-media-types`**: normalizes wrong manifest media-type declarations (OPF-029, e.g. a jpg stamped `image/png` by an aggregator). Attribute-only and quote-preserving; it fires only when the file's magic bytes confirm the extension, so a misnamed-but-consistent file (a PNG renamed .jpg) is never made worse.
|
|
73
73
|
- **`--fix-cover`**: repairs dangling EPUB2 cover wiring. A `<meta name="cover">` whose content names no manifest item is re-pointed when the OPF guide's own cover reference names an existing item, and removed when nothing identifies it; EPUB3 `properties="cover-image"` is deliberately audit-only (guessing which image is the cover is not deterministic). Cover wiring is invisible to epubcheck, so cover-only repairs are accepted under the same no-worse bar the lossy strips use.
|
|
74
74
|
- **`--fix-comment-double-hyphen`**: replaces `--` sequences inside XML comments with en-dashes. `--` is illegal inside an XML comment (epubcheck fatal RSC-016: the book opens in lenient readers but never passes epubcheck). The edit stays inside the comment bodies: text nodes and CDATA sections are never touched, and each comment's terminator is left intact.
|
|
75
|
+
- **`--fix-svg-dup-ids`**: renames duplicate `id` values inside standalone `.svg` entries (old calibre SVG page renders repeat glyph ids within one document; the intake wave that found this carried 445 to 1,339 'Duplicate "glNNNN"' errors per book). The first occurrence keeps its name, later ones gain `_2`/`_3`, and internal references are deliberately untouched: they already resolve to the first definition under every reader's first-match lookup.
|
|
76
|
+
- **`--fix-cdata-terminator`**: completes a truncated CDATA-terminator comment in an inline `<style>` block (`/*]]>` becomes `/*]]>*/`). A comment-wrapped CDATA block whose closing `*/` the converter dropped leaves the CSS parser inside an unterminated comment: one CSS-008 'Premature end of file' per affected document. Only the malformed-terminator token class is rewritten; invalid CSS stays unfixed, and stylesheet files are not touched.
|
|
77
|
+
- **`--fix-misnested-inline`**: rewrites the Mobipocket drop-cap mis-nest `<i><b>X</i></b>` to `<i><b>X</b></i>` (em/strong variants and either tag order). Only the both-open-then-reversed-close shape is touched; a punctuation run caught between the reversed closers (`<i><b>xile</i>,</b>`) survives outside both spans, and a letter run there is refused (which span keeps the letters has no deterministic answer).
|
|
78
|
+
- **`--fix-stray-close`**: removes an end tag whose element has no open start tag anywhere above it (the stray extra `</div>` on a shell page, which cascades into 'body must be terminated by the matching end-tag'). Merely mis-nested pairs are never touched: a close is removed only when nothing by that name is open.
|
|
79
|
+
- **`--fix-unterminated-attr`**: closes an attribute value left open to the tag's own `>` (`<p class="footnote>` becomes `<p class="footnote">`). The parser swallows prose into the value until the next quote and dies on the first `<` it meets. Guards keep it off legal `>`-bearing values: bare-word values only, and no quote may close before the next `<`.
|
|
75
80
|
|
|
76
81
|
|
|
77
82
|
Four opt-in fixes are **lossy** and stand apart from the semantics-preserving rest:
|
|
@@ -79,7 +84,7 @@ Four opt-in fixes are **lossy** and stand apart from the semantics-preserving re
|
|
|
79
84
|
- **`--strip-pagination`**: remove print page numbers and running headers that a PDF/OCR conversion baked into the body text as literal paragraphs (so they reflow into the middle of a sentence: "where the hay cart **16** was taking him"). It removes only that injected furniture, never the author's prose: where a number split a sentence it rejoins the two paragraphs (closing up a word split like `compli-` / `mentary`), and it preserves roman chapter numbers, page-list nav anchors, and years. A roman numeral counts as a page number only when it is well-formed and under 100, so ordinary words (`mid`, `mix`, `civil`) are never mistaken for pages. A book is only treated as paginated when it has both a dense run of arabic numbers and several confident mid-sentence interrupts, so a merely chapter-numbered book is left alone. Three safety nets guard every edit (character conservation, tag balance, and an epubcheck no-regression check); any failure leaves the document untouched.
|
|
80
85
|
- **`--strip-broken-tags`**: remove leaked HTML closing tags missing their open brackets (e.g. `</p>`) that render as raw text.
|
|
81
86
|
- **`--strip-watermarks`**: remove known producer and distributor watermarks (e.g. OceanofPDF, ABC Amber LIT Converter) and stray marker files. It locates the stamp and deletes the outermost wrapper whose entire visible text is the watermark, ensuring prose that merely mentions the URL is preserved. An inline stamp link is deleted only when it holds nothing but the stamp; a match too large to be safe (an unclosed stamp anchor that swallowed prose) is refused and reported for manual repair rather than deleted.
|
|
82
|
-
- **`--strip-stub-docs`**: drop spine documents whose entire visible text is one identical short placeholder repeated across the spine: the Bookmate-style export whose chapters are all the same "content unavailable" notice, passing epubcheck because the placeholder is valid XHTML. The drop cascades (archive entries, manifest items, spine order, NCX navPoints, nav toc entries), so the book opens straight into its real chapters. The identity rule is deliberately conservative (same text, at least 3 docs, at least 30% of the spine, each doc short but not blank) and a book whose *every* spine doc is the stub is refused outright: that book is empty and needs a re-source, never a repair.
|
|
87
|
+
- **`--strip-stub-docs`**: drop spine documents whose entire visible body text is one identical short placeholder repeated across the spine: the Bookmate-style export whose chapters are all the same "content unavailable" notice, passing epubcheck because the placeholder is valid XHTML. The drop cascades (archive entries, manifest items, spine order, NCX navPoints, nav toc entries), so the book opens straight into its real chapters. The identity rule is deliberately conservative (same body text, at least 3 docs, at least 30% of the spine, each doc short but not blank; the pool is the `<body>` span, and image-carrier pages with no body text are exempt, so illustrated part-divider pages are never candidates) and a book whose *every* spine doc is the stub is refused outright: that book is empty and needs a re-source, never a repair.
|
|
83
88
|
|
|
84
89
|
All lossy edits are invisible to epubcheck, so they are accepted when the result is *no worse* rather than measurably better.
|
|
85
90
|
|
|
@@ -24,7 +24,7 @@ bindery-cli makes accidentally broken markup well-formed again. It does not rewr
|
|
|
24
24
|
|
|
25
25
|
Everything below is off until its flag is passed (or all at once via `--all`). They go further than well-formedness (altering structure or adding minimal content), so they never run by default:
|
|
26
26
|
|
|
27
|
-
- **`--fix-ids`**: rewrite ids that are not valid XML names (start with a digit, contain a colon) in the OPF manifest, updating every reference to them (spine, fallback, media-overlay, the EPUB 2 cover meta), and in the NCX (where old conversions stamp navPoint ids from UUIDs). Touches the OPF, so it is off by default; the dc: metadata is never altered.
|
|
27
|
+
- **`--fix-ids`**: rewrite ids that are not valid XML names (start with a digit, contain a colon, or carry any character outside the XML NameChar set, like the apostrophes calibre copies from filenames) in the OPF manifest, updating every reference to them (spine, fallback, media-overlay, the EPUB 2 cover meta), and in the NCX (where old conversions stamp navPoint ids from UUIDs). Touches the OPF, so it is off by default; the dc: metadata is never altered.
|
|
28
28
|
- **`--add-img-alt`**: add `alt=""` to `<img>` elements missing the required attribute. Renders identically, but it adds markup the author never wrote, and an empty alt tells a screen reader the image is decorative; hence opt-in.
|
|
29
29
|
- **`--reserialize`**: rebuild content documents that are still malformed by re-parsing them with html5lib and re-emitting XHTML, closing unclosed `<p>`/`<div>`/`<span>`/`<blockquote>` that the regex transforms cannot. Runs only on documents that are not already well-formed, so good files are untouched.
|
|
30
30
|
- **`--strip-bad-attrs`**: drop attributes that are invalid XML (a name starting with a digit, or a namespaced name whose prefix is never declared, like Office VML `v:shapes`). Surgical and a no-op on well-formed files.
|
|
@@ -45,6 +45,11 @@ Everything below is off until its flag is passed (or all at once via `--all`). T
|
|
|
45
45
|
- **`--fix-media-types`**: normalizes wrong manifest media-type declarations (OPF-029, e.g. a jpg stamped `image/png` by an aggregator). Attribute-only and quote-preserving; it fires only when the file's magic bytes confirm the extension, so a misnamed-but-consistent file (a PNG renamed .jpg) is never made worse.
|
|
46
46
|
- **`--fix-cover`**: repairs dangling EPUB2 cover wiring. A `<meta name="cover">` whose content names no manifest item is re-pointed when the OPF guide's own cover reference names an existing item, and removed when nothing identifies it; EPUB3 `properties="cover-image"` is deliberately audit-only (guessing which image is the cover is not deterministic). Cover wiring is invisible to epubcheck, so cover-only repairs are accepted under the same no-worse bar the lossy strips use.
|
|
47
47
|
- **`--fix-comment-double-hyphen`**: replaces `--` sequences inside XML comments with en-dashes. `--` is illegal inside an XML comment (epubcheck fatal RSC-016: the book opens in lenient readers but never passes epubcheck). The edit stays inside the comment bodies: text nodes and CDATA sections are never touched, and each comment's terminator is left intact.
|
|
48
|
+
- **`--fix-svg-dup-ids`**: renames duplicate `id` values inside standalone `.svg` entries (old calibre SVG page renders repeat glyph ids within one document; the intake wave that found this carried 445 to 1,339 'Duplicate "glNNNN"' errors per book). The first occurrence keeps its name, later ones gain `_2`/`_3`, and internal references are deliberately untouched: they already resolve to the first definition under every reader's first-match lookup.
|
|
49
|
+
- **`--fix-cdata-terminator`**: completes a truncated CDATA-terminator comment in an inline `<style>` block (`/*]]>` becomes `/*]]>*/`). A comment-wrapped CDATA block whose closing `*/` the converter dropped leaves the CSS parser inside an unterminated comment: one CSS-008 'Premature end of file' per affected document. Only the malformed-terminator token class is rewritten; invalid CSS stays unfixed, and stylesheet files are not touched.
|
|
50
|
+
- **`--fix-misnested-inline`**: rewrites the Mobipocket drop-cap mis-nest `<i><b>X</i></b>` to `<i><b>X</b></i>` (em/strong variants and either tag order). Only the both-open-then-reversed-close shape is touched; a punctuation run caught between the reversed closers (`<i><b>xile</i>,</b>`) survives outside both spans, and a letter run there is refused (which span keeps the letters has no deterministic answer).
|
|
51
|
+
- **`--fix-stray-close`**: removes an end tag whose element has no open start tag anywhere above it (the stray extra `</div>` on a shell page, which cascades into 'body must be terminated by the matching end-tag'). Merely mis-nested pairs are never touched: a close is removed only when nothing by that name is open.
|
|
52
|
+
- **`--fix-unterminated-attr`**: closes an attribute value left open to the tag's own `>` (`<p class="footnote>` becomes `<p class="footnote">`). The parser swallows prose into the value until the next quote and dies on the first `<` it meets. Guards keep it off legal `>`-bearing values: bare-word values only, and no quote may close before the next `<`.
|
|
48
53
|
|
|
49
54
|
|
|
50
55
|
Four opt-in fixes are **lossy** and stand apart from the semantics-preserving rest:
|
|
@@ -52,7 +57,7 @@ Four opt-in fixes are **lossy** and stand apart from the semantics-preserving re
|
|
|
52
57
|
- **`--strip-pagination`**: remove print page numbers and running headers that a PDF/OCR conversion baked into the body text as literal paragraphs (so they reflow into the middle of a sentence: "where the hay cart **16** was taking him"). It removes only that injected furniture, never the author's prose: where a number split a sentence it rejoins the two paragraphs (closing up a word split like `compli-` / `mentary`), and it preserves roman chapter numbers, page-list nav anchors, and years. A roman numeral counts as a page number only when it is well-formed and under 100, so ordinary words (`mid`, `mix`, `civil`) are never mistaken for pages. A book is only treated as paginated when it has both a dense run of arabic numbers and several confident mid-sentence interrupts, so a merely chapter-numbered book is left alone. Three safety nets guard every edit (character conservation, tag balance, and an epubcheck no-regression check); any failure leaves the document untouched.
|
|
53
58
|
- **`--strip-broken-tags`**: remove leaked HTML closing tags missing their open brackets (e.g. `</p>`) that render as raw text.
|
|
54
59
|
- **`--strip-watermarks`**: remove known producer and distributor watermarks (e.g. OceanofPDF, ABC Amber LIT Converter) and stray marker files. It locates the stamp and deletes the outermost wrapper whose entire visible text is the watermark, ensuring prose that merely mentions the URL is preserved. An inline stamp link is deleted only when it holds nothing but the stamp; a match too large to be safe (an unclosed stamp anchor that swallowed prose) is refused and reported for manual repair rather than deleted.
|
|
55
|
-
- **`--strip-stub-docs`**: drop spine documents whose entire visible text is one identical short placeholder repeated across the spine: the Bookmate-style export whose chapters are all the same "content unavailable" notice, passing epubcheck because the placeholder is valid XHTML. The drop cascades (archive entries, manifest items, spine order, NCX navPoints, nav toc entries), so the book opens straight into its real chapters. The identity rule is deliberately conservative (same text, at least 3 docs, at least 30% of the spine, each doc short but not blank) and a book whose *every* spine doc is the stub is refused outright: that book is empty and needs a re-source, never a repair.
|
|
60
|
+
- **`--strip-stub-docs`**: drop spine documents whose entire visible body text is one identical short placeholder repeated across the spine: the Bookmate-style export whose chapters are all the same "content unavailable" notice, passing epubcheck because the placeholder is valid XHTML. The drop cascades (archive entries, manifest items, spine order, NCX navPoints, nav toc entries), so the book opens straight into its real chapters. The identity rule is deliberately conservative (same body text, at least 3 docs, at least 30% of the spine, each doc short but not blank; the pool is the `<body>` span, and image-carrier pages with no body text are exempt, so illustrated part-divider pages are never candidates) and a book whose *every* spine doc is the stub is refused outright: that book is empty and needs a re-source, never a repair.
|
|
56
61
|
|
|
57
62
|
All lossy edits are invisible to epubcheck, so they are accepted when the result is *no worse* rather than measurably better.
|
|
58
63
|
|
|
@@ -1,4 +1,105 @@
|
|
|
1
1
|
# bindery-cli Patch Notes
|
|
2
|
+
|
|
3
|
+
## v0.47.1 (2026-10-06)
|
|
4
|
+
|
|
5
|
+
### The two-level help: compact grouped --help, full --help-repairs reference
|
|
6
|
+
|
|
7
|
+
The per-verb help had grown into a wall: a 16-line usage line listing all 27
|
|
8
|
+
repair flags, then roughly 30 paragraph-length descriptions in one flat
|
|
9
|
+
options list with the safe, structural, and lossy classes visually
|
|
10
|
+
undifferentiated, and the shared set printed a third time in the top-level
|
|
11
|
+
help. Flag names, semantics, and the argparse surface are untouched
|
|
12
|
+
(CalibreQuarry's consent mirror drives the shipped parser); this release
|
|
13
|
+
reorganizes how they are presented.
|
|
14
|
+
|
|
15
|
+
* One inventory, two renderings: the repair flags now live in a single
|
|
16
|
+
table (dest, one-line short help, long reference text) that registers
|
|
17
|
+
the compact `--help` into titled argparse groups and renders
|
|
18
|
+
`--help-repairs` as the full reference with every flag's long
|
|
19
|
+
description. Structural repairs are grouped by defect class (markup and
|
|
20
|
+
nesting; references and resources; package wiring), and adding a flag
|
|
21
|
+
now means adding a table row.
|
|
22
|
+
* Short usage lines replace the flag wall (`bindery repair [options] path
|
|
23
|
+
[output]`), repair and library carry examples epilogs, and the
|
|
24
|
+
top-level help becomes a verb list plus a pointer to the two levels.
|
|
25
|
+
* The four lossy strips are consistently `LOSSY:`-prefixed (two of four
|
|
26
|
+
carried it before) and live in their own warning-titled section ("these
|
|
27
|
+
delete converter-injected content; accepted on a no-worse bar").
|
|
28
|
+
* library's own 13 flags are grouped (run control, candidate selection,
|
|
29
|
+
backups, output and library integration) instead of one flat list.
|
|
30
|
+
* `--help-repairs` is a custom argparse action, deliberately not
|
|
31
|
+
store_true, so the wiring test's repair-selection count ignores it;
|
|
32
|
+
TestHelpLayout (five tests) pins the usage lines, group titles, LOSSY
|
|
33
|
+
prefixes, reference content, and that guarantee.
|
|
34
|
+
|
|
35
|
+
Tests: 620 (up from 615).
|
|
36
|
+
|
|
37
|
+
## v0.47.0 (2026-10-06)
|
|
38
|
+
|
|
39
|
+
### The intake-wave batch: five repair surfaces from five open issues
|
|
40
|
+
|
|
41
|
+
The 2026-10-05/06 acquisition wave filed five issues against real books; all
|
|
42
|
+
five land here. Two are classifier and validity corrections to existing
|
|
43
|
+
surfaces, three are new opt-in structural repairs, and each was verified
|
|
44
|
+
epubcheck-gated against its own fixture before shipping.
|
|
45
|
+
|
|
46
|
+
* The stub classifier pools BODY text and exempts image carriers (issue #1).
|
|
47
|
+
The four Kobo part-divider pages of the Great Change carry only a
|
|
48
|
+
full-bleed `<img>` each, and their identical head `<title>` welded them,
|
|
49
|
+
with the title page, into a fake five-doc stub class: emptytext answered
|
|
50
|
+
PARTIAL and `--strip-stub-docs` offered a strip that deleted real
|
|
51
|
+
illustrations (the apply-time gate caught the deletion, but the offer was
|
|
52
|
+
wrong). Both the analyzer and the repair twin now pool the `<body>` span,
|
|
53
|
+
and a doc with no body text that carries an image is furniture, never a
|
|
54
|
+
candidate; identical titles alone are never a stub class.
|
|
55
|
+
* `--fix-ids` sees every invalid character, not just colons and digit leads
|
|
56
|
+
(issue #3). The Lisey's Story OPF carries ids like
|
|
57
|
+
`Stephen_King_-_Lisey's_story22` (apostrophes copied from filenames),
|
|
58
|
+
which passed the original empty/colon/leading-char NCName check
|
|
59
|
+
wholesale, so a full `--all` sweep left the RSC-005 id errors standing
|
|
60
|
+
(epubcheck's message says "without colons"; the defect was never colons).
|
|
61
|
+
The check now enforces the XML NameChar set at every position, and the
|
|
62
|
+
rename sanitizes every invalid character to `_`, references in sync as
|
|
63
|
+
before. Fixture: 22 ids renamed, the OPF id error class cleared; the
|
|
64
|
+
errors that remain on that book are unrelated classes (deprecated body
|
|
65
|
+
attributes, stray GeoXML elements).
|
|
66
|
+
* `--fix-svg-dup-ids` (issue #2): old calibre 0.8.x SVG page renders repeat
|
|
67
|
+
glyph ids within one document. The first occurrence keeps its name, later
|
|
68
|
+
ones gain `_2`/`_3`, and references are deliberately untouched: they
|
|
69
|
+
resolve to the first definition under every reader's first-match lookup,
|
|
70
|
+
so first-keeps-id preserves exactly what rendered. Standalone `.svg`
|
|
71
|
+
entries only; content-document ids are styled and anchor-targeted and
|
|
72
|
+
stay out of scope. Fixture: 346 ids rewritten, 80 aggregated errors to
|
|
73
|
+
zero.
|
|
74
|
+
* `--fix-cdata-terminator` (issue #4): completes the truncated `/*]]>`
|
|
75
|
+
CDATA-terminator comment inside inline `<style>` blocks (the 2002-era
|
|
76
|
+
conversion lineage whose 19 content docs each died with CSS-008
|
|
77
|
+
"Premature end of file"; the aggregated gate saw one message). Only the
|
|
78
|
+
malformed-terminator token class: invalid CSS stays unfixed, stylesheet
|
|
79
|
+
files are not touched, and an intact closer (even space-separated) is
|
|
80
|
+
left alone.
|
|
81
|
+
* The markup-fatal trio (issue #5): `--fix-misnested-inline` normalizes the
|
|
82
|
+
Mobipocket drop-cap mis-nest `<i><b>X</i></b>` (em/strong variants and
|
|
83
|
+
either order; punctuation between the reversed closers survives outside
|
|
84
|
+
both spans; a letter run is refused as ambiguous),
|
|
85
|
+
`--fix-stray-close` removes an end tag whose element has no open start
|
|
86
|
+
frame (a merely mis-nested pair is never touched; script/style text is
|
|
87
|
+
skipped to its end tag and an unclosed comment stops the walk), and
|
|
88
|
+
`--fix-unterminated-attr` closes an
|
|
89
|
+
attribute value open to the tag's own `>` (`<p class="footnote>`;
|
|
90
|
+
bare-word values only, and only when no quote closes before the next
|
|
91
|
+
`<`). Life (Keith Richards): 3 fatals to zero, gate-accepted with the
|
|
92
|
+
unterminated-attr repair running before the tag-shape readers an open
|
|
93
|
+
value would otherwise lie to.
|
|
94
|
+
|
|
95
|
+
Tests: 615 (up from 575), including a real-core row per new flag, the
|
|
96
|
+
wiring-table pins for the five new dests, classifier regressions for both
|
|
97
|
+
sides of the stub fix, and a version-sync pin for uv.lock's root entry
|
|
98
|
+
(the pre-release audit found v0.46.1 shipped with the lock one patch
|
|
99
|
+
stale, a class the pyproject pin cannot see). The plugin ships the default pass only; it
|
|
100
|
+
vendors the changed modules byte-identically and CI byte-compiles it under
|
|
101
|
+
3.11 through 3.14.
|
|
102
|
+
|
|
2
103
|
## v0.46.1 (2026-10-02)
|
|
3
104
|
|
|
4
105
|
### The closing sweep: documentation truth after the 0.46 line
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bindery-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.47.1"
|
|
8
8
|
description = "Audit EPUB body text and repair broken EPUBs with deterministic, epubcheck-gated fixes; atomic in-place replacement in a Calibre library."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
classifiers = [
|
|
@@ -40,6 +40,7 @@ The plugin vendors the core and never gates (the epubcheck latency ruling).
|
|
|
40
40
|
| Waves 15 + blitz | The plugin PEP-758 HIGH + the 3.11-3.14 interpreter matrix, analyzer robustness, the per-matcher census (possessive bounding; cited by tests/test_matcher_hardening.py), stdlib XML hardening, FastDaemon v2 (reconcile-not-retire), `--strip-stub-docs`, `bindery doctor` + `repair --json`, `--encode-url-spaces` entry renames, the cover + tocdrift analyzers, the privacy/GitHub/docs/comment/hygiene batches, requires-python 3.12 with markers, the em-dash sweep | v0.40.0-v0.44.0 |
|
|
41
41
|
| 0.45 | Structured fix records (`fixes`, `ncx_uid_synced`, `watermark_refusals`) in every JSON payload; CalibreQuarry consumes them with the 0.45.0 floor | v0.45.0 |
|
|
42
42
|
| 0.46 | RSC-016 `--fix-comment-double-hyphen` (the last open repair class) and the RepairFlags dataclass | v0.46.0 |
|
|
43
|
+
| 0.47 | The intake-wave batch (five open issues): the stub classifier's body-pool + image-carrier exemption, the NCName-complete `--fix-ids`, and `--fix-svg-dup-ids`, `--fix-cdata-terminator`, `--fix-misnested-inline`, `--fix-stray-close`, `--fix-unterminated-attr` | v0.47.0 |
|
|
43
44
|
|
|
44
45
|
## Open work
|
|
45
46
|
|
|
@@ -13,13 +13,14 @@ restyle, re-compress, or restructure content, and it does not attempt to fix arb
|
|
|
13
13
|
schema (RSC-005) violations, which are usually harmless to readers and not safely
|
|
14
14
|
mechanizable.
|
|
15
15
|
|
|
16
|
-
The deliberate exceptions to "semantics-preserving" come in opt-in groups. The
|
|
16
|
+
The deliberate exceptions to "semantics-preserving" come in opt-in groups. The twenty-one
|
|
17
17
|
**structural repairs** (`--fix-empty-body`, `--fix-missing-title`, `--fix-id-colons`,
|
|
18
18
|
`--fix-page-map`, `--strip-epub3-attrs`, `--downgrade-epub3-tags`,
|
|
19
19
|
`--unwrap-block-in-inline`, `--strip-invalid-value`, `--unwrap-illegal-tags`,
|
|
20
20
|
`--prune-missing-resources`, `--strip-broken-anchors`, `--encode-url-spaces`,
|
|
21
|
-
`--fix-container`, `--fix-media-types`, `--fix-cover`,
|
|
22
|
-
`--fix-
|
|
21
|
+
`--fix-container`, `--fix-media-types`, `--fix-cover`, `--fix-comment-double-hyphen`,
|
|
22
|
+
`--fix-svg-dup-ids`, `--fix-cdata-terminator`, `--fix-misnested-inline`,
|
|
23
|
+
`--fix-stray-close`, `--fix-unterminated-attr`) alter
|
|
23
24
|
markup structure or fabricate minimal content; the four
|
|
24
25
|
**lossy modes** (`--strip-pagination`, `--strip-broken-tags`, `--strip-watermarks`,
|
|
25
26
|
`--strip-stub-docs`) remove
|
|
@@ -124,7 +125,7 @@ byte-for-byte. The normal gate applies.
|
|
|
124
125
|
|
|
125
126
|
### Opt-in: structural repairs
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
Twenty-one repairs go past well-formedness and therefore require their own flag; none is ever
|
|
128
129
|
part of the default pipeline:
|
|
129
130
|
|
|
130
131
|
- **`--fix-empty-body`**: ` ` inside a strictly empty `<body></body>` ("body
|
|
@@ -233,8 +234,54 @@ use, with the `partial` rule intact.
|
|
|
233
234
|
never touched, and each comment's `-->` terminator stays intact. An unclosed comment is a
|
|
234
235
|
different fatal class and is left alone. It applies to content documents, the NCX, and
|
|
235
236
|
the OPF alike, and every replaced sequence is counted.
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
- **`--fix-svg-dup-ids`**: rename duplicate `id` values inside standalone `.svg`
|
|
238
|
+
entries (old calibre 0.8.x SVG page renders repeat glyph ids within one document;
|
|
239
|
+
four books from one intake wave carried 445 to 1,339 'Duplicate "glNNNN"' RSC-005s
|
|
240
|
+
each). The first occurrence keeps its name; every later one gains `_2`, `_3`, ... in
|
|
241
|
+
document order (prefixed with `_` until the name is free). Internal
|
|
242
|
+
`xlink:href="#x"` / `url(#x)` references are deliberately never rewritten: they
|
|
243
|
+
already resolve to the first definition under every reader's first-match lookup, so
|
|
244
|
+
first-keeps-id preserves exactly what rendered. Scope is standalone `.svg` entries
|
|
245
|
+
only: content-document ids are styled by CSS selectors and targeted by anchors, and
|
|
246
|
+
renaming those needs a reference graph this repair does not build.
|
|
247
|
+
- **`--fix-cdata-terminator`**: complete a truncated CDATA-terminator comment in an
|
|
248
|
+
inline `<style>` block: `/*]]>` becomes `/*]]>*/` (a comment-wrapped CDATA block
|
|
249
|
+
whose closing `*/` the converter dropped; epubcheck's CSS parser reads the dangling
|
|
250
|
+
`/*` as an unterminated comment and answers CSS-008 'Premature end of file' once per
|
|
251
|
+
affected document, 19 identical errors on the 2002-era fixture). Deliberately narrow:
|
|
252
|
+
only the malformed-terminator token class, inside `<style>` blocks only; invalid CSS
|
|
253
|
+
properties and selectors stay unfixed (renderers degrade gracefully), stylesheet FILE
|
|
254
|
+
entries are not touched, and a closer that already sits intact (possibly after
|
|
255
|
+
whitespace) is left alone.
|
|
256
|
+
- **`--fix-misnested-inline`**: rewrite the Mobipocket drop-cap mis-nest
|
|
257
|
+
`<i><b>X</i></b>` to `<i><b>X</b></i>` (em/strong variants and either tag order
|
|
258
|
+
included). Only the both-open-then-reversed-close shape is rewritten: any match is
|
|
259
|
+
XML fatal, and swapping the closers is the unique well-formed form carrying the same
|
|
260
|
+
two spans over the same text. The Exile variant, where a short punctuation run sits
|
|
261
|
+
between the reversed closers (`<i><b>xile</i>,</b>`), is covered: the punctuation
|
|
262
|
+
survives outside both spans, byte-preserved. A letter run between the closers
|
|
263
|
+
(`<i><b>S</i>ome</b>`, where `<b>` spans `Some` and `<i>` only `S`) is refused: which
|
|
264
|
+
span keeps the letters has no deterministic answer. Same-tag pairs, attribute-bearing
|
|
265
|
+
starts, and markup crossing the run are untouched.
|
|
266
|
+
- **`--fix-stray-close`**: remove an end tag whose element has no open start tag
|
|
267
|
+
anywhere above it (the stray extra `</div>` on a shell page, which cascades into
|
|
268
|
+
'body must be terminated by the matching end-tag'). The rule is deliberately narrower
|
|
269
|
+
than a nesting repair: a close is removed only when NOTHING by that name is open, so
|
|
270
|
+
a merely mis-nested pair (`<div><span></div></span>`) is left exactly as written
|
|
271
|
+
(its `</span>` still has an open frame and is never misread as stray). Self-closed
|
|
272
|
+
and void starts open no frame; raw-text elements (`script`, `style`) are skipped to
|
|
273
|
+
their end tag so JS/CSS text is never read as markup; the walk stops at an unclosed
|
|
274
|
+
comment or CDATA opener, where the parser stops reading markup too.
|
|
275
|
+
- **`--fix-unterminated-attr`**: close an attribute value left open to the tag's own
|
|
276
|
+
`>` (`<p class="footnote>` -> `<p class="footnote">`; the parser swallows prose into
|
|
277
|
+
the value until the next quote, meets a `<`, and dies with the RSC-016 fatal "the
|
|
278
|
+
value of attribute ... must not contain the '<' character"). Three guards keep it off
|
|
279
|
+
legal `>`-bearing values: the value is one bare word, the `>` must be followed by no
|
|
280
|
+
quote before the next `<` (a real multi-line value closes quote-first), and only
|
|
281
|
+
double-quoted values are judged. It runs before the other tag-shape readers, which an
|
|
282
|
+
open value otherwise lies to.
|
|
283
|
+
|
|
284
|
+
Nineteen of the twenty-one are evaluated by the normal `gate`: unlike the lossy strips,
|
|
238
285
|
their benefit is visible to epubcheck (they clear errors), so a run with no measurable
|
|
239
286
|
improvement is a noop and nothing is applied. The two exceptions are `--fix-cover` and
|
|
240
287
|
`--encode-url-spaces`, whose gains sit on axes epubcheck does not measure (cover
|
|
@@ -267,7 +314,7 @@ Since this removes visible text from the reading experience, it is lossy by desi
|
|
|
267
314
|
Strips known producer and redistributor watermarks out of EPUBs (e.g. OceanofPDF.com, ABC Amber LIT Converter). The removal is a balanced-element surgery rather than regex slicing: it locates the stamp and deletes the outermost wrapper whose *entire visible text* is the watermark, ensuring prose that merely mentions the URL is preserved. Also drops known zero-byte marker files. Like other lossy operations, this is verified via `no_worse`.
|
|
268
315
|
|
|
269
316
|
### Opt-in, lossy: stub-document strip (`--strip-stub-docs`)
|
|
270
|
-
Drops spine documents whose entire visible text is one identical short placeholder repeated across the spine: the Bookmate/DRM-sample export whose chapters are all the same "content unavailable" notice, valid XHTML that epubcheck passes. The identity rule mirrors the emptytext analyzer's placeholder signals and is deliberately conservative: a candidate doc's visible text is 12-600 characters (short, not blank) and IDENTICAL to the text of at least two other spine docs, and the class covers at least 30% of the spine; only the single most common repeated class qualifies.
|
|
317
|
+
Drops spine documents whose entire visible BODY text is one identical short placeholder repeated across the spine: the Bookmate/DRM-sample export whose chapters are all the same "content unavailable" notice, valid XHTML that epubcheck passes. The identity rule mirrors the emptytext analyzer's placeholder signals and is deliberately conservative: a candidate doc's visible body text is 12-600 characters (short, not blank) and IDENTICAL to the text of at least two other spine docs, and the class covers at least 30% of the spine; only the single most common repeated class qualifies. The pool is the `<body>` span, not the whole document (whole document when a doc has none; a converter stamps the same head `<title>` on every shell page, which used to weld unrelated docs into a fake stub class), and an image-carrier page (empty body text, an `<img>`/`<image>` present) is exempt outright: those are illustrated part-divider pages, furniture the strip must never delete (the Great Change misfire, 2026-10-05).
|
|
271
318
|
|
|
272
319
|
The drop cascades fully: the placeholder archive entries, their manifest items and spine itemrefs (with every package edge rewritten), their NCX navPoints (the always-on playOrder resequencing heals the sequence afterward), and their nav toc `<li>` entries. Refusals, each reported as a no-op: no repeated class, the class under the fraction bar, and the class covering the whole spine (a book whose every spine doc is the same stub is EMPTY; it needs a re-source, never a repair). Accepted under the `no_worse` bar with the partial rule intact.
|
|
273
320
|
|
|
@@ -11,7 +11,7 @@ Calibre plugin built from this core.
|
|
|
11
11
|
Single source of truth for the version. pyproject.toml must match VERSION below.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "0.
|
|
14
|
+
VERSION = "0.47.1"
|
|
15
15
|
__version__ = VERSION
|
|
16
16
|
|
|
17
17
|
from .epub import RepairReport, repair_epub # noqa: E402
|
|
@@ -1224,6 +1224,28 @@ def _visible_chars(html: str) -> int:
|
|
|
1224
1224
|
return len(_visible_text(html))
|
|
1225
1225
|
|
|
1226
1226
|
|
|
1227
|
+
# Stub identity pools BODY text (the image-carrier fix, 2026-10-06): a
|
|
1228
|
+
# converter stamps the same head <title> on every shell page, so
|
|
1229
|
+
# whole-document text welded unrelated docs into a fake stub class (the Great
|
|
1230
|
+
# Change divider-page misfire, issue #1). An image-carrier rule (empty body
|
|
1231
|
+
# text, an <img>/<image> present) marks divider pages: furniture, never
|
|
1232
|
+
# stubs. Mirrored by epub._stub_body_text / epub._stub_is_image_carrier.
|
|
1233
|
+
_BODY_SPAN_RE = re.compile(r"<body\b[^>]*>(.*)</body>", re.IGNORECASE | re.DOTALL)
|
|
1234
|
+
_IMAGE_CARRIER_RE = re.compile(r"<(?:img|image)\b", re.IGNORECASE)
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def _body_visible_text(html: str) -> str:
|
|
1238
|
+
"""Visible text of the <body> span (whole document when there is none)."""
|
|
1239
|
+
m = _BODY_SPAN_RE.search(html)
|
|
1240
|
+
return _visible_text(m.group(1)) if m else _visible_text(html)
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def _is_image_carrier(html: str, body_text: str) -> bool:
|
|
1244
|
+
"""True for a doc with no body text that carries an image element: a
|
|
1245
|
+
divider page (furniture), never a stub."""
|
|
1246
|
+
return body_text == "" and bool(_IMAGE_CARRIER_RE.search(html))
|
|
1247
|
+
|
|
1248
|
+
|
|
1227
1249
|
def analyze_emptytext(book: Book) -> dict:
|
|
1228
1250
|
"""Count visible text across the (pre-read) spine and gather triage signals."""
|
|
1229
1251
|
texts = book.visible_texts()
|
|
@@ -1235,10 +1257,23 @@ def analyze_emptytext(book: Book) -> dict:
|
|
|
1235
1257
|
# short stub repeated across a large fraction of the spine (most chapters
|
|
1236
1258
|
# replaced by an identical "content unavailable" notice). Blank docs are
|
|
1237
1259
|
# excluded by PLACEHOLDER_STUB_MIN so well-made books full of small section
|
|
1238
|
-
# dividers (each with distinct text) do not trip it.
|
|
1260
|
+
# dividers (each with distinct text) do not trip it. The pool is the BODY
|
|
1261
|
+
# text, and image-carrier pages are excluded outright: the four Kobo
|
|
1262
|
+
# divider pages of the Great Change carry only a full-bleed <img>, and
|
|
1263
|
+
# their identical head <title> used to be the "stub" every one of them
|
|
1264
|
+
# matched (offering a --strip-stub-docs that deleted real illustrations).
|
|
1239
1265
|
sig = any(_PLACEHOLDER_SIG.search(t) for t in texts)
|
|
1266
|
+
body_texts = []
|
|
1267
|
+
carriers = []
|
|
1268
|
+
for d in book.spine:
|
|
1269
|
+
doc_html = book.docs.get(d, "")
|
|
1270
|
+
body = _body_visible_text(doc_html)
|
|
1271
|
+
body_texts.append(body)
|
|
1272
|
+
carriers.append(_is_image_carrier(doc_html, body))
|
|
1240
1273
|
stubs = Counter(
|
|
1241
|
-
t
|
|
1274
|
+
t
|
|
1275
|
+
for t, carrier in zip(body_texts, carriers, strict=True)
|
|
1276
|
+
if not carrier and PLACEHOLDER_STUB_MIN <= len(t) <= PLACEHOLDER_STUB_MAX
|
|
1242
1277
|
)
|
|
1243
1278
|
stub_n = stubs.most_common(1)[0][1] if stubs else 0
|
|
1244
1279
|
spine_n = max(1, len(book.spine))
|