Skip to content

feat(formats): add EPUB input - #55

Merged
chaliy merged 25 commits into
mainfrom
claude/project-thread-3z7tjs
Oct 27, 2026
Merged

chaliy merged 25 commits into
mainfrom
claude/project-thread-3z7tjs

Conversation

@chaliy

@chaliy chaliy commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

What changed

New epub feature (implies html, in all_formats, so CLI and Python/Node bindings get it) converts EPUB 2/3 e-books (.epub, application/epub+zip, sniffed from the mimetype entry) to Markdown:

  • Chapters render in spine order, separated by ---. Each chapter goes through the HTML parser, so it gets the same stripping, escaping and hidden-content rules (S20) as web pages.
  • Each spine document is read at most once; linear="no" items such as covers and notes are skipped.
  • The title comes from OPF dc:title, and stats.chapters counts rendered chapters.
  • hrefs are resolved against the OPF directory and percent-decoded. Paths that climb above the archive root are ignored.
  • If META-INF/encryption.xml encrypts a spine document (DRM), extraction fails with Error::EpubExtract instead of emitting ciphertext. Font obfuscation alone is fine.
  • A missing manifest item or archive entry gives a missing_chapter warning.
  • New Format::Epub ("epub"), Error::EpubExtract, ExtractionStats::chapters and WarningCode::MissingChapter.

Package XML and all chapters share the call's Limits budget (nodes, timeout, depth, output cap) through the bounded ZIP and XML readers.

Why

EPUB is the last format in the cheap-formats roadmap initiative (text/Markdown, CSV/TSV, ipynb, EML and ODT are already merged).

Before / After

$ markkit extract --file b.epub   # 2 chapters; ch1 has <script>, ch2 a display:none paragraph
# before
Error: Unsupported format: ...
# after
# One

It was *dark*.

---

# Two

End.

I ran tests locally under all features, the default build, --no-default-features, docx,spreadsheet,markdown,email,odt,epub and epub alone; clippy passes with -D warnings. epub pulls in no tokio or reqwest.

Risk

  • Low: the feature is additive and opt-in; the default build is unchanged.
  • Chapters reuse the HTML parser, so HTML parser behavior changes will show up in EPUB output.

Checklist

  • Tests added or updated: 14 tests in epub_integration cover spine order, duplicates, non-linear items, missing chapters, OPF fallback and detection. Security cases cover DRM rejection, path traversal, zip bombs, limits (max_nodes, timeout, output cap), hidden content and markup escaping.
  • Backward compatibility considered: the change is additive. T6 covers EPUB, and S20 lists it. Knowledge, CHANGELOG and README are updated.

Requested by Михайло · project thread


Generated by Claude Code

chaliy added 25 commits October 26, 2026 23:35
Plain text (always on) goes through the IR: paragraphs on blank lines,
hard breaks on single newlines. Markdown (new `markdown` feature, in
all_formats) is re-parsed with pulldown-cmark into the IR and rendered
back, so raw HTML never passes through. Both decode UTF-8/UTF-16 BOMs,
drop control characters and warn on lossy decoding. Neither is sniffed
from bytes. Threat model T7, invariant S13.
…-3z7tjs

# Conflicts:
#	CHANGELOG.md
#	README.md
#	crates/markkit/Cargo.toml
#	crates/markkit/src/extract.rs
#	crates/markkit/src/lib.rs
#	knowledge/log.md
#	knowledge/security/threat-model.md
…-3z7tjs

# Conflicts:
#	.github/workflows/ci.yml
#	crates/markkit/Cargo.toml
#	crates/markkit/src/extract.rs
#	crates/markkit/src/lib.rs
#	knowledge/log.md
#	knowledge/security/threat-model.md
…-3z7tjs

# Conflicts:
#	knowledge/log.md
#	knowledge/security/threat-model.md
Always-on RFC 4180 reader renders delimited text as one GFM table.
Delimiter is tab for TSV and detected among , ; tab | for CSV. Reading
is bounded by max_sheet_rows / max_sheet_cols (truncated_table warning);
unterminated quotes emit malformed_csv. Adds Format::Tsv.
…-3z7tjs

# Conflicts:
#	CHANGELOG.md
#	README.md
#	crates/markkit/src/extract.rs
#	knowledge/formats/format-support.md
#	knowledge/log.md
#	knowledge/security/threat-model.md
nbformat 4 notebooks render Markdown cells through the Markdown path,
code cells as fenced blocks tagged with the kernel language, and text
outputs and tracebacks (ANSI stripped) as text blocks; rich outputs are
skipped. Adds Format::Notebook, Error::NotebookExtract and
ExtractionStats::cells.
With no format feature, extract_document rendered unbounded, so always-on
text, CSV and notebook input ignored max_output_size and the timeout
(csv_integration::security_timeout_applies failed under
--no-default-features). Bounded rendering is now unconditional, decoding is
followed by a deadline check, and CI tests --no-default-features.
# Conflicts:
#	crates/markkit/src/extract.rs
#	crates/markkit/src/format.rs
#	crates/markkit/src/lib.rs
#	crates/markkit/src/limits.rs
#	crates/markkit/src/render/markdown.rs
#	knowledge/log.md
#	knowledge/operations/testing.md
New email feature (mail-parser 0.11) renders subject, From/To/Cc/Date,
the plain-text body (HTML alternative through the HTML parser when no
plain part exists) and an attachment list. Header values are folded so
encoded newlines cannot inject lines; attachments are never parsed.
Adds Format::Eml, Error::EmailExtract, ExtractionStats::attachments.
…-3z7tjs

# Conflicts:
#	crates/markkit-js/src/lib.rs
#	crates/markkit/src/extract.rs
#	knowledge/formats/format-support.md
New odt feature reuses the bounded ZIP and XML readers from DOCX:
headings (outline level, styled, Title), bold/italic/strike from text
properties with run merging, links, ordered/bulleted lists, tables with
repeat counts capped by the sheet limits. Notes, annotations, tracked
changes and frames are skipped. Adds Format::Odt (sniffed from the ODF
mimetype) and Error::OdtExtract.
New epub feature (implies html) reads the OPF package, renders spine
chapters in order through the HTML parser (each once, linear=no
skipped), takes the title from dc:title and rejects DRM-encrypted spine
documents. hrefs that escape the archive root are ignored. Adds
Format::Epub (sniffed from the mimetype entry), Error::EpubExtract and
ExtractionStats::chapters.
@chaliy chaliy self-assigned this Oct 27, 2026
@chaliy
chaliy merged commit 79c4e94 into main Oct 27, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant