epub-codec 0.0.0 → 1.0.1

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.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -0
  3. package/dist/codec.cjs +28 -0
  4. package/dist/codec.d.cts +2344 -0
  5. package/dist/codec.d.ts +2344 -0
  6. package/dist/codec.js +25 -0
  7. package/dist/diagnostics-Ee4kG7mw.d.cts +60 -0
  8. package/dist/diagnostics-Ee4kG7mw.d.ts +60 -0
  9. package/dist/diagnostics.cjs +96 -0
  10. package/dist/diagnostics.d.cts +2 -0
  11. package/dist/diagnostics.d.ts +2 -0
  12. package/dist/diagnostics.js +85 -0
  13. package/dist/format.cjs +9 -0
  14. package/dist/format.d.cts +6 -0
  15. package/dist/format.d.ts +6 -0
  16. package/dist/format.js +6 -0
  17. package/dist/image/dimensions.cjs +82 -0
  18. package/dist/image/dimensions.d.cts +11 -0
  19. package/dist/image/dimensions.d.ts +11 -0
  20. package/dist/image/dimensions.js +79 -0
  21. package/dist/index.cjs +105 -0
  22. package/dist/index.d.cts +32 -0
  23. package/dist/index.d.ts +32 -0
  24. package/dist/index.js +32 -0
  25. package/dist/nav/nav3.cjs +19 -0
  26. package/dist/nav/nav3.d.cts +4 -0
  27. package/dist/nav/nav3.d.ts +4 -0
  28. package/dist/nav/nav3.js +18 -0
  29. package/dist/nav/ncx.cjs +17 -0
  30. package/dist/nav/ncx.d.cts +4 -0
  31. package/dist/nav/ncx.d.ts +4 -0
  32. package/dist/nav/ncx.js +16 -0
  33. package/dist/nav/reconcile.cjs +8 -0
  34. package/dist/nav/reconcile.d.cts +4 -0
  35. package/dist/nav/reconcile.d.ts +4 -0
  36. package/dist/nav/reconcile.js +7 -0
  37. package/dist/nav/write.cjs +39 -0
  38. package/dist/nav/write.d.cts +10 -0
  39. package/dist/nav/write.d.ts +10 -0
  40. package/dist/nav/write.js +37 -0
  41. package/dist/ocf/container.cjs +20 -0
  42. package/dist/ocf/container.d.cts +4 -0
  43. package/dist/ocf/container.d.ts +4 -0
  44. package/dist/ocf/container.js +19 -0
  45. package/dist/ocf/write.cjs +13 -0
  46. package/dist/ocf/write.d.cts +4 -0
  47. package/dist/ocf/write.d.ts +4 -0
  48. package/dist/ocf/write.js +12 -0
  49. package/dist/opf/metadata.cjs +51 -0
  50. package/dist/opf/metadata.d.cts +7 -0
  51. package/dist/opf/metadata.d.ts +7 -0
  52. package/dist/opf/metadata.js +50 -0
  53. package/dist/opf/parse.cjs +47 -0
  54. package/dist/opf/parse.d.cts +6 -0
  55. package/dist/opf/parse.d.ts +6 -0
  56. package/dist/opf/parse.js +46 -0
  57. package/dist/opf/types.cjs +0 -0
  58. package/dist/opf/types.d.cts +20 -0
  59. package/dist/opf/types.d.ts +20 -0
  60. package/dist/opf/types.js +0 -0
  61. package/dist/opf/write.cjs +55 -0
  62. package/dist/opf/write.d.cts +12 -0
  63. package/dist/opf/write.d.ts +12 -0
  64. package/dist/opf/write.js +54 -0
  65. package/dist/path.cjs +22 -0
  66. package/dist/path.d.cts +5 -0
  67. package/dist/path.d.ts +5 -0
  68. package/dist/path.js +20 -0
  69. package/dist/read.cjs +166 -0
  70. package/dist/read.d.cts +10 -0
  71. package/dist/read.d.ts +10 -0
  72. package/dist/read.js +164 -0
  73. package/dist/util/base64.cjs +51 -0
  74. package/dist/util/base64.d.cts +5 -0
  75. package/dist/util/base64.d.ts +5 -0
  76. package/dist/util/base64.js +49 -0
  77. package/dist/write.cjs +136 -0
  78. package/dist/write.d.cts +10 -0
  79. package/dist/write.d.ts +10 -0
  80. package/dist/write.js +134 -0
  81. package/dist/xhtml/context.cjs +0 -0
  82. package/dist/xhtml/context.d.cts +20 -0
  83. package/dist/xhtml/context.d.ts +20 -0
  84. package/dist/xhtml/context.js +0 -0
  85. package/dist/xhtml/footnote.cjs +32 -0
  86. package/dist/xhtml/footnote.d.cts +7 -0
  87. package/dist/xhtml/footnote.d.ts +7 -0
  88. package/dist/xhtml/footnote.js +29 -0
  89. package/dist/xhtml/inline.cjs +128 -0
  90. package/dist/xhtml/inline.d.cts +11 -0
  91. package/dist/xhtml/inline.d.ts +11 -0
  92. package/dist/xhtml/inline.js +127 -0
  93. package/dist/xhtml/list-id.cjs +22 -0
  94. package/dist/xhtml/list-id.d.cts +13 -0
  95. package/dist/xhtml/list-id.d.ts +13 -0
  96. package/dist/xhtml/list-id.js +20 -0
  97. package/dist/xhtml/read.cjs +423 -0
  98. package/dist/xhtml/read.d.cts +20 -0
  99. package/dist/xhtml/read.d.ts +20 -0
  100. package/dist/xhtml/read.js +422 -0
  101. package/dist/xhtml/style-constants.cjs +13 -0
  102. package/dist/xhtml/style-constants.d.cts +8 -0
  103. package/dist/xhtml/style-constants.d.ts +8 -0
  104. package/dist/xhtml/style-constants.js +8 -0
  105. package/dist/xhtml/write.cjs +183 -0
  106. package/dist/xhtml/write.d.cts +12 -0
  107. package/dist/xhtml/write.d.ts +12 -0
  108. package/dist/xhtml/write.js +182 -0
  109. package/dist/xml/build.cjs +46 -0
  110. package/dist/xml/build.d.cts +5 -0
  111. package/dist/xml/build.d.ts +5 -0
  112. package/dist/xml/build.js +46 -0
  113. package/dist/xml/entities.cjs +20 -0
  114. package/dist/xml/entities.d.cts +5 -0
  115. package/dist/xml/entities.d.ts +5 -0
  116. package/dist/xml/entities.js +18 -0
  117. package/dist/xml/node.cjs +67 -0
  118. package/dist/xml/node.d.cts +83 -0
  119. package/dist/xml/node.d.ts +83 -0
  120. package/dist/xml/node.js +58 -0
  121. package/dist/xml/parse.cjs +88 -0
  122. package/dist/xml/parse.d.cts +5 -0
  123. package/dist/xml/parse.d.ts +5 -0
  124. package/dist/xml/parse.js +88 -0
  125. package/dist/xml/query.cjs +44 -0
  126. package/dist/xml/query.d.cts +11 -0
  127. package/dist/xml/query.d.ts +11 -0
  128. package/dist/xml/query.js +37 -0
  129. package/dist/zip.cjs +19 -0
  130. package/dist/zip.d.cts +9 -0
  131. package/dist/zip.d.ts +9 -0
  132. package/dist/zip.js +17 -0
  133. package/package.json +95 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joseph Mearman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # epub-codec
2
+
3
+ [![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&logoColor=white)](https://github.com/ExaDev/documents.js/tree/main/packages/epub-codec) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](https://www.npmjs.com/package/epub-codec) [![npm version](https://img.shields.io/npm/v/epub-codec)](https://www.npmjs.com/package/epub-codec) [![CI](https://img.shields.io/github/actions/workflow/status/ExaDev/documents.js/ci.yml?branch=main)](https://github.com/ExaDev/documents.js/actions)
4
+
5
+ > A hand-written, dependency-minimal EPUB 2/3 codec: reads flowable EPUB 2 and EPUB 3 packages into the shared [document-schema.js](../document-schema.js/README.md) content pivot, and writes deterministic, minimal EPUB 3. Built on [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser), [fflate](https://github.com/101arrowz/fflate), and [Zod 4](https://zod.dev).
6
+
7
+ An EPUB decomposes into two things this family already does well: an OCF ZIP container with a manifest and a fixed first entry — structurally `odf.js`/`ooxml.js` territory — whose payload is flowable block content in well-formed XHTML — semantically `markdown-codec` territory, since EPUB 3.3 requires its content documents to be real XML, not tag-soup HTML. This package sits at exactly that intersection: its OCF/ZIP layer and XML parse/build wrapper mirror the conventions those siblings already established (hand-duplicated, not imported — see [Architecture](#architecture) for why), and its XHTML-to-`ContentDocument` mapping is the closest relative `markdown-codec`'s own AST-to-`ContentDocument` lowering has in this family.
8
+
9
+ ```mermaid
10
+ graph TD
11
+ schema("document-schema.js")
12
+ epubcodec("epub-codec")
13
+
14
+ schema --> epubcodec
15
+
16
+ click schema "https://github.com/ExaDev/documents.js/tree/main/packages/document-schema.js" "document-schema.js"
17
+ click epubcodec "https://github.com/ExaDev/documents.js/tree/main/packages/epub-codec" "epub-codec"
18
+
19
+ style epubcodec fill:#f9a825,stroke:#333,stroke-width:3px
20
+ ```
21
+
22
+ `epub-codec` depends on nothing else in this family beyond `document-schema.js` — see [Dependency choices](#dependency-choices) for why it does _not_ depend on `archive-codec` or `byte-codec`, both real candidates the issue that created this package asked to be checked. Wiring this package into `documents.js`'s conversion engine, `document-cli`, `document-mcp`, or the web UI is explicitly out of scope here — see [ExaDev/documents.js#802](https://github.com/ExaDev/documents.js/issues/802), a separate, already-filed follow-up blocked on this package existing at all.
23
+
24
+ ## Scope
25
+
26
+ **Flowable EPUB only.** Fixed-layout EPUB (FXL, `rendition:layout-pre-paginated`) is fixed-page geometry — closer to `pdf-codec`'s own private layout representation than to flowable content — and is not specially detected or rejected; an FXL package's XHTML still reads as ordinary flowable content, just without the positioning its `properties="rendition:layout-pre-paginated"` metadata was asking a reading system to honour.
27
+
28
+ **Read EPUB 2 and EPUB 3. Write EPUB 3 only.** `readEpub`/`readEpubContent` accept either; `writeEpub`/`writeEpubContent` always produce a minimal, spec-valid EPUB 3 package, matching how this family already treats legacy format quirks elsewhere (read, never re-emitted).
29
+
30
+ ## Getting started
31
+
32
+ Requires Node.js `>=20` and pnpm `11.6.0` (pinned via `packageManager` in `package.json`).
33
+
34
+ ```sh
35
+ pnpm install
36
+ ```
37
+
38
+ Install as a dependency in another project:
39
+
40
+ ```sh
41
+ pnpm add epub-codec
42
+ # or
43
+ npm install epub-codec
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ Reading and writing EPUB bytes, at the tree level (the primary API — [document-schema.js](../document-schema.js/README.md)'s `DocumentTree`, matching `markdown-codec`'s identical dual-level convention):
49
+
50
+ ```ts
51
+ import { readEpub, writeEpub } from "epub-codec";
52
+
53
+ const tree = readEpub(epubBytes); // -> DocumentTree, kind: 'wordprocessing'
54
+ // tree.children is one section group per spine itemref, in spine order -- headings, lists, and
55
+ // footnote/blockquote constructs already promoted into their own groups by document-schema.js's assembleTree.
56
+ const bytes = writeEpub(tree); // a fresh, minimal, spec-valid EPUB 3
57
+ ```
58
+
59
+ The flat pair one level down (`ContentDocument`, the codec-exchange shape every reader/writer in this family actually reads and writes):
60
+
61
+ ```ts
62
+ import { readEpubContent, writeEpubContent } from "epub-codec";
63
+
64
+ const { sections, metadata } = readEpubContent(epubBytes); // ContentDocument, kind: 'wordprocessing'
65
+ const bytes = writeEpubContent({ kind: "wordprocessing", metadata, sections });
66
+ ```
67
+
68
+ Both accept an optional `sink` (`EpubDiagnosticSink`, called once per recoverable read issue or unrepresentable construct — see [Conventions](#conventions) for the three-tier policy this shares with `markdown-codec`/`pdf-codec`):
69
+
70
+ ```ts
71
+ import { readEpubContent } from "epub-codec";
72
+
73
+ const document = readEpubContent(epubBytes, {
74
+ sink: (diagnostic) => console.warn(diagnostic.code, diagnostic.message),
75
+ });
76
+ ```
77
+
78
+ The same round trip as a schema-validated [`z.codec()`](https://zod.dev) pair, mirroring `markdown-codec`'s `markdownCodec`/`markdownContentCodec`:
79
+
80
+ ```ts
81
+ import { z } from "zod";
82
+ import { epubCodec, epubContentCodec } from "epub-codec";
83
+
84
+ const tree = z.decode(epubCodec, epubBytes); // throws a ZodError if epubBytes has no zip header
85
+ const bytes2 = z.encode(epubCodec, tree);
86
+ ```
87
+
88
+ This is the no-extra-options form only — `readEpub(Content)`/`writeEpub(Content)` remain the entry points for a diagnostic sink.
89
+
90
+ One spine itemref becomes one `ContentSection`, in spine order — **every** itemref, including one marked `linear="no"` (an EPUB 2 idiom for supplementary content, most often a footnote/endnote page): this package reads it as an ordinary section rather than silently skipping real content a reading system happens to route around. EPUB has no page concept of its own, so every section is given the same invented A4 + 1in default geometry (`ReadEpubOptions`/`WriteEpubOptions` carry no override for this, unlike `markdown-codec`'s `pageSize`/`margins` options — nothing in this package's own scope needs one yet).
91
+
92
+ ## Architecture
93
+
94
+ Layered from the lossless OCF/XML primitives outward to the XHTML-to-`ContentDocument` mapping itself:
95
+
96
+ - **`src/zip.ts`** — the OCF ZIP container: fixed-mtime, ordered-entries `zipPackage`/`unzipPackage` over `fflate`, hand-duplicated from `ooxml.js`'s and `odf.js`'s own identical wrappers rather than depending on `archive-codec` — see [Dependency choices](#dependency-choices).
97
+ - **`src/xml/`** — the lossless XML layer every other module in this package builds on: `parse.ts`/`build.ts` wrap `fast-xml-parser` with the identical `preserveOrder` configuration `ooxml.js`'s and `odf.js`'s own XML modules use (order and mixed content survive; entity encoding stays raw until `entities.ts` decodes it in the one place text actually becomes content), `query.ts` the same handful of tree-walking helpers (`rootElement`, `findChildElement`, `childrenWithTag`, `elementsWithTag`, `attrValue`, plus `findElement` and `textContent`, needed here for footnote-target and nav-toc resolution respectively).
98
+ - **`src/ocf/`** — `container.ts` resolves the OPF rootfile from `META-INF/container.xml` (EPUB 3.3 §6.7.2); `write.ts` is its structural inverse.
99
+ - **`src/opf/`** — `parse.ts`/`write.ts` read and write the OPF package document (§5.4): Dublin Core metadata (`metadata.ts`), the manifest, and the spine.
100
+ - **`src/nav/`** — `nav3.ts`/`ncx.ts` reduce the EPUB 3 `<nav epub:type="toc">` document and the EPUB 2 NCX to a flat, fragment-stripped href sequence each; `reconcile.ts` compares that sequence against the spine's own reading order (the issue's own explicit "the spine wins" decision); `write.ts` builds a minimal EPUB 3 nav document, one entry per section, titled from each section's own first heading.
101
+ - **`src/xhtml/`** — the mapping this package exists for: `read.ts` (`readXhtmlBody`, one XHTML content document's `<body>` to `ContentBlock[]`), `write.ts` (`writeXhtmlBody`, its structural inverse, built on `document-schema.js`'s own `decomposeSection` rather than re-deriving heading/list/construct nesting by hand), `inline.ts` (run-level formatting and footnote reference extents), `footnote.ts` (EPUB 3 `epub:type="noteref"`/`"footnote"` and the EPUB 2 linked-anchor idiom, both mapped onto the identical `anchor` construct), `list-id.ts` (list marker type packed into the opaque `numId`, mirroring `markdown-codec`'s own mechanism for the identical schema gap), `context.ts`/`style-constants.ts` (shared read-side plumbing and round-trip-only styleId/font constants).
102
+ - **`src/image/dimensions.ts`** — PNG/JPEG format detection and pixel dimensions, hand-written rather than reused from `byte-codec` — see [Dependency choices](#dependency-choices).
103
+ - **`src/util/base64.ts`** — isomorphic base64 codec, a third hand-written copy of the identical helper `odf.js`'s and `pdf-codec`'s own `src/util/base64.ts` already carry.
104
+ - **`src/path.ts`** — package-relative path resolution (manifest hrefs against the OPF's own directory, `<img src>` against its own XHTML document's directory), honouring `../` segments.
105
+ - **`src/diagnostics.ts`** — the three-tier read/write failure policy; see [Conventions](#conventions).
106
+ - **`src/read.ts`/`src/write.ts`** — the public entry points, composing every layer above into `readEpub(Content)`/`writeEpub(Content)`.
107
+ - **`src/codec.ts`** — `epubCodec`/`epubContentCodec`, the `z.codec()` pair.
108
+
109
+ ## Dependency choices
110
+
111
+ The issue that created this package ([ExaDev/documents.js#801](https://github.com/ExaDev/documents.js/issues/801)) named `archive-codec` and `byte-codec` as real candidates for this package's own OCF/ZIP and image-dimension layers, and asked for the choice to be investigated and justified rather than assumed either way. Both were investigated; both were declined, for reasons specific to what each actually offers today rather than as a blanket "hand-write everything" reflex:
112
+
113
+ - **Not `archive-codec` for the ZIP layer.** `archive-codec`'s own `zip/container.ts` is structurally identical to what this package needs (`zipPackage`/`unzipPackage`, ordered entries, a `stored` flag) — but it does not pin a fixed entry mtime, unlike `ooxml.js`'s and `odf.js`'s own `zip.ts`, both of which do exactly to keep output byte-deterministic across two builds of identical content. Nothing in this family writes through `archive-codec`'s own ZIP writer today, so the gap has never mattered before; it would matter here, since byte-deterministic output is this package's own explicit requirement. Every existing format codec in this family (`ooxml.js`, `odf.js`) already hand-duplicates this exact wrapper rather than sharing it — precisely to keep each codec's release cadence decoupled from a package it would otherwise need to bump in lockstep with, the same reasoning `odf.js`'s own README states for not depending on `ooxml.js` — so this package's own `src/zip.ts` is a third hand-written copy of the identical ~30-line wrapper, not a fourth pattern. `archive-codec` remains genuinely useful elsewhere in this family (ZIP-in-ZIP walking, CFB reading) — neither of which a flat OCF container needs, since a flowable EPUB has no nested-archive or compound-file embedding to recurse into.
114
+ - **Not `byte-codec` for image dimensions.** `byte-codec`'s `image/jpeg-info.ts` (header-only JPEG dimensions, no sample decoding) would have been a clean reuse for the JPEG half — but its PNG half (`image/png-decode.ts`) is a full pixel decode, normalising every PNG colour type to 8-bit gray/RGB planes for a PDF Image XObject's own needs. This package never needs a single decoded pixel: `ContentImageBlock` carries the image's own raw bytes as base64 and only needs its _dimensions_, so a full PNG decode would pay real CPU per manifest image to read four IHDR bytes, and risks rejecting a real PNG variant an IHDR-only read would tolerate. Depending on `byte-codec` for the JPEG half alone while hand-writing the PNG half would gain nothing over hand-writing both — JPEG marker scanning and PNG IHDR reading are each roughly the same amount of code — so `src/image/dimensions.ts` mirrors `markdown-codec`'s own identical `src/image/image.ts` instead: the one other codec in this family with the identical problem (dimensions from arbitrary image bytes, no format-native explicit sizing to read instead — every OOXML/ODF image anchor already carries its own explicit display size).
115
+
116
+ ## Conventions
117
+
118
+ - **Hand-written, dependency-minimal**, matching every sibling codec's own stated bet: no `epubjs`/`epub-gen`/`epub2`/`node-epub`/general-purpose ZIP library dependency (`adm-zip`, `jszip`, `yazl`, `yauzl`), enforced by `eslint.config.ts`'s own `no-restricted-imports` patterns.
119
+ - **Worker-isomorphic**: runtime `src/` must not import `node:*`, a bare Node builtin, or use the `Buffer` global — enforced statically by the shared `no-restricted-imports`/`no-restricted-globals` guard and dynamically by `pnpm test:workers` (the whole read/write pipeline exercised inside a real Cloudflare Workers isolate via `@cloudflare/vitest-pool-workers`). `writeEpubContent`'s generated identifier uses `crypto.randomUUID()` (the Web Crypto API, globally available in Node 20+/Workers/browsers), never `node:crypto`.
120
+ - **A three-tier read/write failure policy**, matching `markdown-codec`'s/`pdf-codec`'s own: throw a typed `EpubParseError`/`EpubWriteError` subclass for input this package cannot meaningfully process at all (an invalid mimetype entry, a missing/unparsable `container.xml`/OPF, an empty spine, unbalanced construct markers, a non-`'wordprocessing'` document); report an `EpubDiagnostic` through the caller's own sink for a recoverable producer mistake or an individual construct this package's own mapping cannot represent, while the rest of the document still reads. `src/diagnostics-coverage.test.ts` asserts every `EpubDiagnosticCodes` entry is reachable from real input — a code that exists only in a comment is dead documentation, worse than none.
121
+ - **`z.codec()` for the round trip** (`epubCodec`, `epubContentCodec`), matching `markdown-codec`'s pair exactly: each wraps the independently-tested read/write pair at its own level with automatic two-way schema validation (no-options form only).
122
+ - **No type assertions anywhere.** Every loosely-typed value is narrowed through a type guard or Zod parse at the boundary.
123
+ - **Conventional commits**, enforced via commitlint + husky.
124
+
125
+ ## Gotchas and quirks
126
+
127
+ Every construct this package's XHTML mapping cannot represent losslessly is a documented `EpubDiagnosticCodes` entry — see `src/diagnostics.ts` for the full table, and each module's own top-of-file comment for which side (read/write) it belongs to:
128
+
129
+ - **`epub/element-unmapped`** — `<sub>`/`<sup>` degrade to plain text: `document-schema.js`'s `ContentRun` carries no subscript/superscript field at all, a genuine family-wide schema gap (no sibling codec has ever needed one; `ooxml.js`'s own docx reader has no `w:vertAlign` handling either), not something specific to this package.
130
+ - **`epub/link-target-external-only`** — every href, external or same-/cross-document, rides `ContentRun.hyperlink` verbatim rather than building the full internal-target `link` construct bookkeeping `document-schema.js`'s own vocabulary supports (an internal link construct needs a resolved same-document `anchor` target this package does not build for ordinary hyperlinks, only for recognised footnote references — see below). A deliberate scope decision: every href still restores byte-for-byte either way, and the feature this degrades is real semantic internal-navigation fidelity, not content loss.
131
+ - **Footnotes are same-document only.** Both the EPUB 3 structured idiom (`epub:type="noteref"`/`"footnote"`) and the EPUB 2 linked-anchor idiom (a `class="footnote"`/`"noteref"` convention with no `epub:type` vocabulary at all, recognised by `src/xhtml/footnote.ts`) map onto the identical `anchor` construct — a run-level point extent at the reference site, a `constructStart`/blocks/`constructEnd` triple around the body. Recognition never crosses a spine itemref boundary: `document-schema.js`'s own construct-marker contract states a bracket pair can never straddle a block list boundary, and each `ContentSection` is its own block list, so a cross-document footnote (a separate "notes.xhtml", the more common real-world EPUB 2 shape) falls through to the ordinary internal-hyperlink handling above instead of being force-fit into a shape the schema cannot express.
132
+ - **`epub/style-residue`** — a document's own `<head>` style declarations (`<link rel="stylesheet">`, `<style>`) are quarantined verbatim as `SourceResidue` on the owning `ContentSection`, never interpreted: CSS is residue, not content. `writeEpubContent` re-emits it into the written `<head>` on a same-format write (this family's standard restorable-fidelity re-emission contract).
133
+ - **An empty or whitespace-only paragraph (`<p></p>`, `<p> </p>`, or bare whitespace text between two block-level siblings — the common case in any pretty-printed real EPUB) is dropped entirely on read**, matching the same "anonymous block box" rule a browser's own HTML block-formatting context already applies to inter-block whitespace, rather than becoming a bogus empty `ContentParagraph`.
134
+ - **List marker type (bullet vs. ordered, and an `<ol>`'s own non-default `start`) is packed into the opaque `numId`** (`epub{N}:{bullet|ordered}[@{start}]`, `src/xhtml/list-id.ts`), since `ContentListMembership` carries no field of its own for it — the identical mechanism `markdown-codec`'s own `src/shared/list-id.ts` uses for its GFM bullet/ordered distinction, hand-mirrored rather than shared. A numId outside this grammar (odf.js's bare `"list1"`, markdown-codec's own `"md1:bullet"`) is read back as an ordinary bullet list with no declared start.
135
+ - **A `<p>` containing a direct `<img>`** (some producers/editors wrap every floating image in a paragraph tag rather than a `<figure>`) is split at the image: the phrasing content before and after becomes its own paragraph (dropped entirely when empty), the image its own block, in source order — an image nested deeper than a direct child (inside a `<span>`, say) is not detected by this split.
136
+ - **A blockquote containing a heading cannot carry its `division` construct** — a marker extent may not open or close a heading scope, and the last heading inside an extent always leaves one standing (`document-schema.js`'s own constraint) — so it degrades to indent-only structure (still `indentLeftPt`/`styleId: "Quote"`) while the heading keeps its own heading fidelity.
137
+ - **Image dimensions are derived from pixel size via the CSS reference-pixel ratio (1px = 1/96in)**, not read from any `<img width>`/`<img height>` attribute or CSS: an EPUB's own XHTML/CSS carries no reliable point-based sizing of its own, so the image's natural pixel size is the one dimension every manifest image reliably has.
138
+ - **A GIF or SVG manifest image has no representation at all** — `ContentImageBlockSchema`'s own `format` field admits only `"png"`/`"jpeg"`, a family-wide schema constraint, not something this package works around; it degrades to alt text with an `epub/image-format-unsupported` diagnostic.
139
+ - **A table cell's own construct-boundary marker (if a foreign producer's `ContentDocument` carries one) has no XHTML representation on write** and is dropped with a diagnostic — `document-schema.js`'s own `decompose` never descends into a table cell, so a cell's blocks are never grouped the way a section's are, and this package's own reader never emits one there either.
140
+ - **`dc:publisher`/`dc:contributor`/`dc:rights` have no `LayoutMetadata` field to land in** and are reported as `epub/metadata-field-unmapped` rather than silently dropped.
141
+
142
+ ## Fidelity
143
+
144
+ **Semantic fidelity** — headings, paragraphs, lists (nested, bullet/ordered), definition lists, tables, images, hyperlinks, text styling (bold/italic/underline/strike/monospace), blockquotes, pre/code blocks, horizontal rules, figure/figcaption, and footnotes (both EPUB 2 and EPUB 3 idioms) all survive as first-class `document-schema.js` nodes or constructs — see [ExaDev/documents.js#801](https://github.com/ExaDev/documents.js/issues/801) for the full acceptance list this package was built against, and `src/roundtrip.test.ts` for the real end-to-end proof (a hand-built `ContentDocument` covering nearly all of it, written to a genuine EPUB 3 zip and read back unchanged).
145
+
146
+ **Restorable fidelity** — a same-format (EPUB-to-EPUB) round trip re-emits quarantined CSS residue verbatim; every other documented gap above (sub/sup styling, internal-link semantics, cross-document footnotes) is a permanent, structural limit rather than a restorable one, since the source construct itself has nowhere in the schema to ride.
147
+
148
+ **Not byte fidelity.** This package has no lossless byte-level `Package` model the way `ooxml.js`/`odf.js` do — there is no `decodePackage`/`encodePackage` pair, and a read-then-write round trip never reproduces the original bytes (a fresh `dc:identifier` is minted on every write, per this package's own explicit write scope). What _is_ deterministic: two writes of the _same_ `ContentDocument` produce byte-identical zip _layout_ (mimetype-first, stored, ordered entries, fixed mtimes) even though the OPF entry's own compressed bytes differ (the fresh identifier). `src/roundtrip.test.ts` pins the layout invariant directly rather than claiming full byte determinism.
149
+
150
+ ## Build, test, and lint
151
+
152
+ ```sh
153
+ pnpm build # turbo run _build -> tsdown (dist/: ESM + CJS + .d.ts, one file set per src module)
154
+ pnpm typecheck # turbo run _typecheck _typecheck:attw -> tsc -p tsconfig.json && tsc -p tsconfig.node.json, plus attw --pack
155
+ pnpm lint # turbo run _lint -> eslint . --fix --cache --max-warnings 0
156
+ pnpm test # turbo run _test -> vitest run --project unit
157
+ pnpm test:watch # vitest --project unit
158
+ pnpm test:workers # turbo run _test:workers -> vitest run --config vitest.workers.config.ts, inside a real Cloudflare Workers (workerd) isolate
159
+ pnpm test:smoke # turbo run _test:smoke -> rebuilds dist/, then verifies the built ESM/CJS output loads and exposes the public surface
160
+ ```
161
+
162
+ To run a single test file: `pnpm vitest run src/path/to/file.test.ts`.
163
+
164
+ ## Release and publishing
165
+
166
+ Release, CI, and commit-message conventions are all workspace-wide, not package-local — see the [monorepo root README](../../README.md#releases) for the mechanism (topological per-package `semantic-release` via `@exadev/semantic-release-workspace`, OIDC trusted npm publishing, automatic sibling dependency-range rewriting) and its [post-release republishing and attestation](../../README.md#releases) note on the restored GitHub Packages mirrors, npm aliases, and SBOM/provenance signing. Publishing this package for the first time needs the one-time npm trusted-publisher registration the root README's own Releases section describes — organization `ExaDev`, repository `documents.js`, workflow `ci.yml`.
167
+
168
+ ## Contributing
169
+
170
+ Conventional Commits, enforced workspace-wide by commitlint through a root `commit-msg` hook. Work inside `packages/epub-codec/`; see [CONTRIBUTING.md](../../CONTRIBUTING.md) for the shared git hooks and history conventions.
171
+
172
+ ## References
173
+
174
+ - [document-schema.js](../document-schema.js/README.md) — the canonical `ContentDocument`/`DocumentTree` schema and the `decompose`/`flattenTree`/`assembleTree` transform this package's writer is built on.
175
+ - [markdown-codec](../markdown-codec/README.md) — the closest architectural relative: hand-written, AST-to-`ContentDocument` lowering, the identical dual-level (`DocumentTree`/`ContentDocument`) API, the identical three-tier diagnostic policy, and the identical list-`numId`-packing mechanism for the identical schema gap.
176
+ - [ooxml.js](../ooxml.js/README.md) / [odf.js](../odf.js/README.md) — the OCF/ZIP and lossless-XML-layer conventions this package's own `src/zip.ts`/`src/xml/` mirror.
177
+ - [archive-codec](../archive-codec/README.md) — the ZIP-in-ZIP/CFB utility package this package deliberately does not depend on; see [Dependency choices](#dependency-choices).
178
+ - [byte-codec](../byte-codec/README.md) — the byte/image utility package this package deliberately does not depend on for image dimensions; see [Dependency choices](#dependency-choices).
179
+ - [EPUB 3.3](https://www.w3.org/TR/epub-33/) — the current W3C Recommendation this package's own EPUB 3 reading and writing targets.
180
+ - [OCF 1.0 / OPF 2.0.1 / OPS 2.0.1](https://idpf.org/epub/dir) — the legacy IDPF specifications this package's EPUB 2 reading targets (container.xml, the OPF package document, and the NCX navigation format are all unchanged in substance between the two generations).
181
+
182
+ ## License
183
+
184
+ MIT
package/dist/codec.cjs ADDED
@@ -0,0 +1,28 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_read = require("./read.cjs");
3
+ const require_write = require("./write.cjs");
4
+ let zod = require("zod");
5
+ let document_schema_js = require("document-schema.js");
6
+ //#region src/codec.ts
7
+ const ZIP_LOCAL_FILE_HEADER = [
8
+ 80,
9
+ 75,
10
+ 3,
11
+ 4
12
+ ];
13
+ function hasZipHeader(bytes) {
14
+ return ZIP_LOCAL_FILE_HEADER.every((byte, index) => bytes[index] === byte);
15
+ }
16
+ const EpubBytesSchema = zod.z.instanceof(Uint8Array).refine(hasZipHeader, { message: "not a zip archive (no PK\\x03\\x04 header)" });
17
+ const epubCodec = zod.z.codec(EpubBytesSchema, document_schema_js.DocumentTreeSchema, {
18
+ decode: (bytes) => require_read.readEpub(bytes),
19
+ encode: (tree) => require_write.writeEpub(tree)
20
+ });
21
+ const epubContentCodec = zod.z.codec(EpubBytesSchema, document_schema_js.ContentDocumentSchema, {
22
+ decode: (bytes) => require_read.readEpubContent(bytes),
23
+ encode: (document) => require_write.writeEpubContent(document)
24
+ });
25
+ //#endregion
26
+ exports.EpubBytesSchema = EpubBytesSchema;
27
+ exports.epubCodec = epubCodec;
28
+ exports.epubContentCodec = epubContentCodec;