epub-codec 1.2.3 → 1.2.4

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.
package/README.md CHANGED
@@ -132,8 +132,9 @@ Every construct this package's XHTML mapping cannot represent losslessly is a do
132
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
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
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 container's own direct-child `<img>`** (some producers/editors wrap every floating image in a paragraph tag rather than a `<figure>`) is split at the image by `readContainerChildren`: the phrasing content before and after becomes its own paragraph (dropped entirely when empty), the image its own block, in source order. That split fires for every container this package reads transparently through `readContainerChildren` — `<body>` itself, `<p>`, `<li>`, `<blockquote>`, `<figure>`, `<div>`, `<section>`, `<article>`, `<aside>`, and `<nav>` — never only a named handful of them; an `<img>` reached anywhere else — nested inside a `<span>`/`<a>` at any depth, or a _direct_ child of a heading, a `<figcaption>`, a `<dt>`/`<dd>`, or a table cell, none of which route through `readContainerChildren` at all — is instead reached by `src/xhtml/inline.ts`'s own run-building recursion, which by that point has committed to producing a flat run sequence with no block list left to insert a sibling image block into, and degrades to its alt text (or nothing, when it carries none) with an `epub/image-inline-unsupported` diagnostic rather than silently vanishing. [ExaDev/documents.js#994](https://github.com/ExaDev/documents.js/issues/994) closed the four remaining silent gaps this guarantee did not originally cover: a non-empty `<caption>`, a legal direct child of `<table>`, is now read as an ordinary paragraph immediately before the table (`epub/table-caption-unsupported`, since `ContentTable` has no field of its own for a caption's distinct tag; any `<img>` the caption itself carries degrades to alt text exactly like a `<figcaption>`'s own, and any run-level construct the caption's own inline content carries — a footnote reference, most commonly — rides that paragraph's own `constructs` field exactly like any other paragraph's), while an empty or whitespace-only `<caption>` is dropped entirely instead, with no diagnostic, matching this package's own empty-paragraph-drop rule documented above; a `<dl>` wrapping one or more `dt`/`dd` pairs in a `<div>` (legal HTML5, used for a per-entry styling hook) is now recognised by recursing into the `<div>`, with no diagnostic at all, since the wrapper carries no properties of its own to lose (identical to every other `<div>` this package already reads transparently); and any content — a nested `<ul>`/`<ol>`, a bare `<img>`, stray text — sitting directly inside a `<ul>`/`<ol>` rather than inside an `<li>` (not valid HTML5, but a shape real-world converters do emit) is now recovered via `epub/list-content-outside-item`, through the same `readContainerChildren` dispatch an `<li>`'s real children already use: content sitting between or after real `<li>` siblings attaches to the preceding item's own nesting — a stray list shares its numId and increments its level, exactly as if it had been nested correctly — while content sitting before the very first `<li>` has no preceding item to attach to and is instead recovered inheriting whatever list membership its own enclosing context already carries (none, unless the `<ul>`/`<ol>` it sits directly inside is itself nested inside another list's `<li>`), landing in the read result immediately before the list's own real items — matching a browser's rendering order for this shape, though not necessarily its nesting depth when the enclosing list is itself nested. One part of the original finding remains a genuine, permanent structural limit rather than a recovered gap: an `<img>` inside a `<pre>`/`<code>` block still cannot become a real `ContentImageBlock` — this package always reads a `<pre>` as a single text-content paragraph, so there is no block list to insert an image block into, the same constraint `epub/image-inline-unsupported` already names elsewhere (the HTML Standard's own content model for `<pre>` is phrasing content, not plain text, which is exactly why `<a>`/`<code>`/`<span>`/`<img>`/`<br>` are legal, real markup inside one and this package's `readPreRuns`/`readPreText` have to handle each explicitly) — but it no longer vanishes silently either; its alt text (or nothing, when it carries none) is spliced into the extracted text in its place, with an `epub/image-pre-unsupported` diagnostic firing unconditionally — every image inside a `<pre>`, not only one with no alt text.
136
- - **A run-level construct extent (most commonly a footnote reference) carried by inline content built directly into a paragraph is preserved on that paragraph's own `constructs` field wherever such a paragraph is built** — a heading, a table caption, a table cell, a `<dt>`/`<dd>`, and a `<figcaption>` all share `constructsField`, the one helper `src/xhtml/read.ts` funnels every such paragraph through, so a fix applied to one of these shapes cannot silently miss the others. A `<pre>`/`<code>` block's own paragraph shares it too, and round-trips it in both directions, despite never routing through `buildInlineRuns` (a `<pre>`'s content model needs its text preserved verbatim, which `buildInlineRuns`'s own whitespace normalisation would break): on read, `readPreRuns`, a dedicated run-splitting walk reached only once a cheap subtree pre-check confirms the `<pre>` actually carries a recognised footnote reference somewhere inside it, brackets that reference's own text as its own run range exactly like `src/xhtml/inline.ts`'s `appendAnchor` does, rebasing any nested call's own construct indices onto the outer run sequence at the point of the merge; the common case (no footnote reference anywhere in the block) still takes the cheap, single-run `readPreText` path unchanged. Because a construct nested inside a `<pre>` can split its content into more than one run with no bearing on whether the block is itself preformatted, `readPre` also stamps every paragraph it produces with `document-schema.js`'s own `ContentParagraph.preformatted` flag, unconditionally and regardless of run count — the one signal the writer can trust, since inferring "this was a `<pre>`" from run shape (a lone monospace run, say) silently misclassifies a multi-run one as an ordinary paragraph the moment it carries a construct. On write, `isPreBlockParagraph` checks `preformatted` first (falling back to `codeLanguage` or the legacy single-monospace-run-with-a-newline heuristic for a foreign producer's document that sets neither), and a recognised `<pre>` paragraph's own runs and constructs are written through `writePreRunsToNodes` — the `<pre>` twin of the ordinary paragraph writer, sharing its extent-finding walk but writing a run's embedded newline as a literal `"\n"` character rather than splitting it into a `<br/>` the way the ordinary path does; `readPreRuns`/`readPreText` map a `<br>` read back from a `<pre>` straight to the same literal `"\n"`, so either spelling round-trips to identical text. A stray element sitting where only a narrower set of tags is expected is likewise recovered rather than dropped, following the same `readContainerChildren`-plus-diagnostic pattern `epub/list-content-outside-item` already established: a `<dl>` (or one of its `<div>` wrappers) carrying anything other than `dt`/`dd`/`<div>` — a stray `<p>`, stray text, a stray `<img>`, or a non-conformant wrapper like `<section>` used in `<div>`'s own place — is recovered via `epub/definition-list-content-outside-entry` (a non-conformant wrapper's own `dt`/`dd` children lose their distinct term/definition treatment once routed this way, degrading to plain concatenated text — a real fidelity cost, but a text-preserving one). The same treatment applies to a `<table>`: content sitting outside any row, caption, or `<colgroup>` — whether directly inside the `<table>` itself, inside one of its `<thead>`/`<tbody>`/`<tfoot>` row groups (which admit only `tr` and script-supporting children per the HTML Standard), or inside a `<colgroup>` itself (which admits only `<col>` and `<template>` children per the HTML Standard — narrower than the script-supporting category, since a `<colgroup>` does not admit a bare `<script>`) — is recovered immediately before the table via `epub/table-content-unrecognized`; a `<tr>` carrying content outside any `<td>`/`<th>` is recovered as its own cell in the row's own column sequence via `epub/table-row-content-outside-cell`; and a `<table>` carrying more than one `<caption>` (HTML5 permits at most one) has every caption beyond the first read as its own paragraph too, via `epub/table-duplicate-caption`, rather than `findChildElement`'s own first-match-only resolution silently discarding it as it previously did. `<script>`, `<template>`, `<style>`, and `<noscript>` appearing anywhere in `<body>` content are all now skipped by one shared guard (`isInertElement`, `src/xhtml/context.ts`) rather than leaking their own raw content as document prose — none of the four were inert outside `<head>`-level residue quarantine before this fix. `<script>`'s raw JS and `<template>`'s inert DOM subtree are never real content regardless of where they are found, and a body-level `<style>` is CSS, exactly like the `<head>`-level style residue this package already quarantines rather than interprets — none of the three fire a diagnostic, since none of them ever carry anything document-schema.js's vocabulary could represent. `<noscript>` is treated the same conservative way for a different reason: its own children ARE ordinary markup a scripting-disabled reading system would genuinely render, but this package cannot tell that case apart from a producer's own "please enable JavaScript" placeholder from the markup alone — so, unlike the other three, dropping a `<noscript>`'s subtree fires its own `epub/noscript-content-skipped` diagnostic (`reportInertElementSkip`, `src/xhtml/context.ts`) at every site that discards one, naming the potential loss rather than staying silent about it.
135
+ - **A container's own direct-child `<img>`** (some producers/editors wrap every floating image in a paragraph tag rather than a `<figure>`) is split at the image by `readContainerChildren`: the phrasing content before and after becomes its own paragraph (dropped entirely when empty), the image its own block, in source order. That split fires for every container this package reads transparently through `readContainerChildren` — `<body>` itself, `<p>`, `<li>`, `<blockquote>`, `<figure>`, `<div>`, `<section>`, `<article>`, `<aside>`, and `<nav>` — never only a named handful of them; an `<img>` reached anywhere else — nested inside a `<span>`/`<a>` at any depth, or a _direct_ child of a heading (the one container this package still reads via a single `buildInlineRuns` call rather than `readContainerChildren`) — is instead reached by `src/xhtml/inline.ts`'s own run-building recursion, which by that point has committed to producing a flat run sequence with no block list left to insert a sibling image block into, and degrades to its alt text (or nothing, when it carries none) with an `epub/image-inline-unsupported` diagnostic rather than silently vanishing. A `<caption>`, `<dt>`/`<dd>`, `<figcaption>`, and a table cell all route through `readContainerChildren` too ([ExaDev/documents.js#1023](https://github.com/ExaDev/documents.js/issues/1023) — see the dedicated bullet below), so a direct-child `<img>` in any of them now becomes a real `ContentImageBlock` the same way, rather than degrading. [ExaDev/documents.js#994](https://github.com/ExaDev/documents.js/issues/994) closed the four remaining silent gaps this guarantee did not originally cover: a non-empty `<caption>`, a legal direct child of `<table>`, is now read as one or more ordinary blocks immediately before the table (`epub/table-caption-unsupported`, fired once per `<caption>` element regardless of how many blocks it decomposes into, since `ContentTable` has no field of its own for a caption's distinct tag; a run-level construct the caption's own inline content carries — a footnote reference, most commonly — rides its own paragraph's `constructs` field exactly like any other paragraph's), while an empty or whitespace-only `<caption>` is dropped entirely instead, with no diagnostic, matching this package's own empty-paragraph-drop rule documented above; a `<dl>` wrapping one or more `dt`/`dd` pairs in a `<div>` (legal HTML5, used for a per-entry styling hook) is now recognised by recursing into the `<div>`, with no diagnostic at all, since the wrapper carries no properties of its own to lose (identical to every other `<div>` this package already reads transparently); and any content — a nested `<ul>`/`<ol>`, a bare `<img>`, stray text — sitting directly inside a `<ul>`/`<ol>` rather than inside an `<li>` (not valid HTML5, but a shape real-world converters do emit) is now recovered via `epub/list-content-outside-item`, through the same `readContainerChildren` dispatch an `<li>`'s real children already use: content sitting between or after real `<li>` siblings attaches to the preceding item's own nesting — a stray list shares its numId and increments its level, exactly as if it had been nested correctly — while content sitting before the very first `<li>` has no preceding item to attach to and is instead recovered inheriting whatever list membership its own enclosing context already carries (none, unless the `<ul>`/`<ol>` it sits directly inside is itself nested inside another list's `<li>`), landing in the read result immediately before the list's own real items — matching a browser's rendering order for this shape, though not necessarily its nesting depth when the enclosing list is itself nested. One part of the original finding remains a genuine, permanent structural limit rather than a recovered gap: an `<img>` inside a `<pre>`/`<code>` block still cannot become a real `ContentImageBlock` — this package always reads a `<pre>` as a single text-content paragraph, so there is no block list to insert an image block into, the same constraint `epub/image-inline-unsupported` already names elsewhere (the HTML Standard's own content model for `<pre>` is phrasing content, not plain text, which is exactly why `<a>`/`<code>`/`<span>`/`<img>`/`<br>` are legal, real markup inside one and this package's `readPreRuns`/`readPreText` have to handle each explicitly) — but it no longer vanishes silently either; its alt text (or nothing, when it carries none) is spliced into the extracted text in its place, with an `epub/image-pre-unsupported` diagnostic firing unconditionally — every image inside a `<pre>`, not only one with no alt text.
136
+ - **A run-level construct extent (most commonly a footnote reference) carried by inline content built directly into a paragraph is preserved on that paragraph's own `constructs` field wherever such a paragraph is built** — a heading and `readContainerChildren`'s own segment flush both share `constructsField`, the one helper `src/xhtml/read.ts` funnels every such paragraph through, so a fix applied to one cannot silently miss the other. A table caption, a table cell, a `<dt>`/`<dd>`, and a `<figcaption>` get this for free by routing through `readContainerChildren` ([ExaDev/documents.js#1023](https://github.com/ExaDev/documents.js/issues/1023)) rather than needing their own direct call to the helper. A `<pre>`/`<code>` block's own paragraph shares it too, and round-trips it in both directions, despite never routing through `buildInlineRuns` (a `<pre>`'s content model needs its text preserved verbatim, which `buildInlineRuns`'s own whitespace normalisation would break): on read, `readPreRuns`, a dedicated run-splitting walk reached only once a cheap subtree pre-check confirms the `<pre>` actually carries a recognised footnote reference somewhere inside it, brackets that reference's own text as its own run range exactly like `src/xhtml/inline.ts`'s `appendAnchor` does, rebasing any nested call's own construct indices onto the outer run sequence at the point of the merge; the common case (no footnote reference anywhere in the block) still takes the cheap, single-run `readPreText` path unchanged. Because a construct nested inside a `<pre>` can split its content into more than one run with no bearing on whether the block is itself preformatted, `readPre` also stamps every paragraph it produces with `document-schema.js`'s own `ContentParagraph.preformatted` flag, unconditionally and regardless of run count — the one signal the writer can trust, since inferring "this was a `<pre>`" from run shape (a lone monospace run, say) silently misclassifies a multi-run one as an ordinary paragraph the moment it carries a construct. On write, `isPreBlockParagraph` checks `preformatted` first (falling back to `codeLanguage` or the legacy single-monospace-run-with-a-newline heuristic for a foreign producer's document that sets neither), and a recognised `<pre>` paragraph's own runs and constructs are written through `writePreRunsToNodes` — the `<pre>` twin of the ordinary paragraph writer, sharing its extent-finding walk but writing a run's embedded newline as a literal `"\n"` character rather than splitting it into a `<br/>` the way the ordinary path does; `readPreRuns`/`readPreText` map a `<br>` read back from a `<pre>` straight to the same literal `"\n"`, so either spelling round-trips to identical text. A stray element sitting where only a narrower set of tags is expected is likewise recovered rather than dropped, following the same `readContainerChildren`-plus-diagnostic pattern `epub/list-content-outside-item` already established: a `<dl>` (or one of its `<div>` wrappers) carrying anything other than `dt`/`dd`/`<div>` — a stray `<p>`, stray text, a stray `<img>`, or a non-conformant wrapper like `<section>` used in `<div>`'s own place — is recovered via `epub/definition-list-content-outside-entry` (a non-conformant wrapper's own `dt`/`dd` children lose their distinct term/definition treatment once routed this way, degrading to plain concatenated text — a real fidelity cost, but a text-preserving one). The same treatment applies to a `<table>`: content sitting outside any row, caption, or `<colgroup>` — whether directly inside the `<table>` itself, inside one of its `<thead>`/`<tbody>`/`<tfoot>` row groups (which admit only `tr` and script-supporting children per the HTML Standard), or inside a `<colgroup>` itself (which admits only `<col>` and `<template>` children per the HTML Standard — narrower than the script-supporting category, since a `<colgroup>` does not admit a bare `<script>`) — is recovered immediately before the table via `epub/table-content-unrecognized`; a `<tr>` carrying content outside any `<td>`/`<th>` is recovered as its own cell in the row's own column sequence via `epub/table-row-content-outside-cell`; and a `<table>` carrying more than one `<caption>` (HTML5 permits at most one) has every caption beyond the first read as its own paragraph too, via `epub/table-duplicate-caption`, rather than `findChildElement`'s own first-match-only resolution silently discarding it as it previously did. `<script>`, `<template>`, `<style>`, and `<noscript>` appearing anywhere in `<body>` content are all now skipped by one shared guard (`isInertElement`, `src/xhtml/context.ts`) rather than leaking their own raw content as document prose — none of the four were inert outside `<head>`-level residue quarantine before this fix. `<script>`'s raw JS and `<template>`'s inert DOM subtree are never real content regardless of where they are found, and a body-level `<style>` is CSS, exactly like the `<head>`-level style residue this package already quarantines rather than interprets — none of the three fire a diagnostic, since none of them ever carry anything document-schema.js's vocabulary could represent. `<noscript>` is treated the same conservative way for a different reason: its own children ARE ordinary markup a scripting-disabled reading system would genuinely render, but this package cannot tell that case apart from a producer's own "please enable JavaScript" placeholder from the markup alone — so, unlike the other three, dropping a `<noscript>`'s subtree fires its own `epub/noscript-content-skipped` diagnostic (`reportInertElementSkip`, `src/xhtml/context.ts`) at every site that discards one, naming the potential loss rather than staying silent about it.
137
+ - **A `<caption>`, `<dt>`, `<dd>`, `<figcaption>`, `<td>`, or `<th>` now recognises real block-level content rather than flattening it** ([ExaDev/documents.js#1023](https://github.com/ExaDev/documents.js/issues/1023)) — all six are Flow content per the HTML Standard, so a `<pre>`, a nested list, or more than one paragraph inside one is real, conformant markup. These six used to build their content with one bare `buildInlineRuns` call each; a block element reached that way has no case in `buildInlineRuns`'s own dispatch, so it fell into the same treatment as a `<span>`, recursing into its children as ordinary phrasing content — losing a `<pre>`'s own block shape, `preformatted` flag, `codeLanguage`, and verbatim whitespace, and, separately, concatenating more than one paragraph-shaped child into one undelimited run with no break (or even a word boundary) between them: `<td><p>Alpha</p><p>Beta</p></td>` used to read as the single run `"AlphaBeta"`. All six now route through `readContainerChildren` instead, the same general block-container reader `<p>`/`<li>`/`<blockquote>`/`<figure>`/`<div>`/`<section>`/`<article>`/`<aside>`/`<nav>` already use, so a `<pre>` stays preformatted, a nested list stays real list structure, and sibling paragraphs stay distinct. `<dd>`'s own `DEFINITION_BODY_INDENT_PT` offset is threaded through a `BuildState.extraIndentPt` field (additive with blockquote nesting's own `quoteDepth`, mirrored via the identical `withQuote`/`withExtraIndent` pattern) so every paragraph the descent produces picks up the indent, not only a single top-level one; a `<th>`'s own implied bold is threaded through as `readContainerChildren`'s own `baseStyle` parameter, reaching every direct phrasing segment but not content nested inside a further block-level child (a `<table>`-in-a-`<th>`, say) — a narrow, documented degrade for a genuinely rare shape, not a silent one.
137
138
  - **A CDATA section (`<![CDATA[...]]>`) is read exactly like an ordinary text node everywhere in the XHTML reading path** — `xml/node.ts`'s `isTextLikeNode` is the one shared predicate every text-bearing walk in this package now dispatches on (`buildInlineRuns`, `readContainerChildren`'s own phrasing/block split, `readPre`'s three text-extraction paths, every stray-content collector in `read.ts`, and `xml/query.ts`'s own `decodedTextContent`), rather than each walk separately checking for `"text"` and quietly excluding `"cdata"`. A CDATA section is simply the alternate XML spelling a producer reaches for when its own literal text would otherwise need escaping (a code sample, or any other content containing a raw `<`/`&`) — exactly the kind of content a DocBook-to-EPUB pipeline or similar tool emits inside a `<pre>` — never a distinct kind of content, so treating the two differently was silently dropping real, well-formed input rather than a corpus-tolerance gap. The one place CDATA is genuinely _not_ interchangeable with a text node is entity decoding: `xml/entities.ts`'s `decodeTextLikeNode` decodes a text node's raw value exactly as before but returns a CDATA node's value untouched, since CDATA content is never subject to XML entity resolution in the first place — running it back through `decodeEntities` would corrupt exactly the unescaped content CDATA exists to carry. `xml/query.ts`'s own plain `textContent` is deliberately narrower and CDATA-blind: it is this package's long-standing published export, and a caller's own `decodeEntities(textContent(x))` idiom stays correct only because that walk never mixes in undecoded-by-design CDATA content for the wrap to misapply entity resolution to — the CDATA-aware, decode-internally behaviour lands as the distinctly named `decodedTextContent` instead, used by `opf/metadata.ts`'s own four Dublin Core readers, so upgrading to CDATA support never silently double-decodes an existing caller's text nodes.
138
139
  - **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.
139
140
  - **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.
package/dist/codec.d.cts CHANGED
@@ -1425,10 +1425,10 @@ declare const epubContentCodec: z.ZodCodec<z.ZodCustom<Uint8Array<ArrayBuffer>,
1425
1425
  endColumn: z.ZodNumber;
1426
1426
  }, z.core.$strip>>;
1427
1427
  type: z.ZodEnum<{
1428
- decimal: "decimal";
1429
- list: "list";
1430
1428
  date: "date";
1431
1429
  custom: "custom";
1430
+ decimal: "decimal";
1431
+ list: "list";
1432
1432
  time: "time";
1433
1433
  whole: "whole";
1434
1434
  textLength: "textLength";
package/dist/codec.d.ts CHANGED
@@ -1425,10 +1425,10 @@ declare const epubContentCodec: z.ZodCodec<z.ZodCustom<Uint8Array<ArrayBuffer>,
1425
1425
  endColumn: z.ZodNumber;
1426
1426
  }, z.core.$strip>>;
1427
1427
  type: z.ZodEnum<{
1428
- decimal: "decimal";
1429
- list: "list";
1430
1428
  date: "date";
1431
1429
  custom: "custom";
1430
+ decimal: "decimal";
1431
+ list: "list";
1432
1432
  time: "time";
1433
1433
  whole: "whole";
1434
1434
  textLength: "textLength";
@@ -34,6 +34,12 @@ function withQuote(state) {
34
34
  quoteDepth: state.quoteDepth + 1
35
35
  };
36
36
  }
37
+ function withExtraIndent(state, pt) {
38
+ return {
39
+ ...state,
40
+ extraIndentPt: state.extraIndentPt + pt
41
+ };
42
+ }
37
43
  function withListItem(state, listItem) {
38
44
  return {
39
45
  ...state,
@@ -48,9 +54,13 @@ function decorateParagraph(paragraph, state) {
48
54
  let decorated = paragraph;
49
55
  if (state.quoteDepth > 0) decorated = {
50
56
  ...decorated,
51
- indentLeftPt: state.quoteDepth * 36,
57
+ indentLeftPt: state.quoteDepth * 36 + state.extraIndentPt,
52
58
  styleId: decorated.styleId ?? "Quote"
53
59
  };
60
+ else if (state.extraIndentPt > 0) decorated = {
61
+ ...decorated,
62
+ indentLeftPt: state.extraIndentPt
63
+ };
54
64
  if (state.listItem !== void 0) {
55
65
  const membership = {
56
66
  numId: state.listItem.numId,
@@ -132,6 +142,7 @@ function readXhtmlBody(xml, options) {
132
142
  context,
133
143
  minter: createIdMinter(),
134
144
  quoteDepth: 0,
145
+ extraIndentPt: 0,
135
146
  listItem: void 0,
136
147
  list: void 0,
137
148
  contentWidthPt: options.contentWidthPt
@@ -165,12 +176,12 @@ const BLOCK_LEVEL_TAGS = /* @__PURE__ */ new Set([
165
176
  "nav",
166
177
  "img"
167
178
  ]);
168
- function readContainerChildren(nodes, state) {
179
+ function readContainerChildren(nodes, state, baseStyle = {}) {
169
180
  const blocks = [];
170
181
  let segment = [];
171
182
  const flush = () => {
172
183
  if (segment.length === 0) return;
173
- const inline = require_xhtml_inline.buildInlineRuns(segment, {}, state.context);
184
+ const inline = require_xhtml_inline.buildInlineRuns(segment, baseStyle, state.context);
174
185
  segment = [];
175
186
  if (inline.runs.every((run) => run.text.trim().length === 0) && inline.constructs.length === 0) return;
176
187
  const paragraph = {
@@ -238,14 +249,7 @@ function readBlockElementInner(element, state) {
238
249
  case "dl": return readDefinitionList(element, state);
239
250
  case "table": return readTable(element, state);
240
251
  case "figure": return readContainerChildren(element.children, state);
241
- case "figcaption": {
242
- const inline = require_xhtml_inline.buildInlineRuns(element.children, {}, state.context);
243
- return [decorateParagraph({
244
- kind: "paragraph",
245
- runs: inline.runs,
246
- ...constructsField(inline)
247
- }, state)];
248
- }
252
+ case "figcaption": return readContainerChildren(element.children, state);
249
253
  case "img": {
250
254
  const block = readImage(element, state);
251
255
  return block === void 0 ? [] : [block];
@@ -450,23 +454,12 @@ function readDefinitionListEntries(nodes, state) {
450
454
  for (const child of nodes) {
451
455
  if (child.type === "element" && child.tag === "dt") {
452
456
  flushStray();
453
- const inline = require_xhtml_inline.buildInlineRuns(child.children, {}, state.context);
454
- blocks.push(decorateParagraph({
455
- kind: "paragraph",
456
- runs: inline.runs,
457
- ...constructsField(inline)
458
- }, state));
457
+ blocks.push(...readContainerChildren(child.children, state));
459
458
  continue;
460
459
  }
461
460
  if (child.type === "element" && child.tag === "dd") {
462
461
  flushStray();
463
- const inline = require_xhtml_inline.buildInlineRuns(child.children, {}, state.context);
464
- blocks.push(decorateParagraph({
465
- kind: "paragraph",
466
- runs: inline.runs,
467
- ...constructsField(inline),
468
- indentLeftPt: 36 + state.quoteDepth * 36
469
- }, state));
462
+ blocks.push(...readContainerChildren(child.children, withExtraIndent(state, 36)));
470
463
  continue;
471
464
  }
472
465
  if (child.type === "element" && child.tag === "div") {
@@ -539,16 +532,11 @@ function readTable(element, state) {
539
532
  if (cellNode.type === "element" && (cellNode.tag === "td" || cellNode.tag === "th")) {
540
533
  flushStrayCell();
541
534
  const cellStyle = cellNode.tag === "th" ? { bold: true } : {};
542
- const inline = require_xhtml_inline.buildInlineRuns(cellNode.children, cellStyle, state.context);
543
- const paragraph = {
544
- kind: "paragraph",
545
- runs: inline.runs,
546
- ...constructsField(inline)
547
- };
535
+ const cellBlocks = readContainerChildren(cellNode.children, state, cellStyle);
548
536
  const colSpan = positiveIntAttr(cellNode, "colspan");
549
537
  const rowSpan = positiveIntAttr(cellNode, "rowspan");
550
538
  cells.push({
551
- blocks: [paragraph],
539
+ blocks: cellBlocks,
552
540
  ...colSpan !== void 0 ? { colSpan } : {},
553
541
  ...rowSpan !== void 0 ? { rowSpan } : {}
554
542
  });
@@ -611,8 +599,8 @@ function collectColgroupStrayContent(section, strayNodes, context) {
611
599
  }
612
600
  }
613
601
  function readTableCaption(captionElement, isDuplicate, state) {
614
- const captionInline = require_xhtml_inline.buildInlineRuns(captionElement.children, {}, state.context);
615
- if (captionInline.runs.every((run) => run.text.trim().length === 0) && captionInline.constructs.length === 0) return [];
602
+ const captionBlocks = readContainerChildren(captionElement.children, state);
603
+ if (captionBlocks.length === 0) return [];
616
604
  if (isDuplicate) state.context.sink({
617
605
  code: require_diagnostics.EpubDiagnosticCodes.TABLE_DUPLICATE_CAPTION,
618
606
  severity: "info",
@@ -625,11 +613,7 @@ function readTableCaption(captionElement, isDuplicate, state) {
625
613
  message: "<caption> has no document-schema.js table-caption field to carry its own distinct tag; read as an ordinary paragraph immediately before the table",
626
614
  href: state.context.sourceHref
627
615
  });
628
- return [decorateParagraph({
629
- kind: "paragraph",
630
- runs: captionInline.runs,
631
- ...constructsField(captionInline)
632
- }, state)];
616
+ return captionBlocks;
633
617
  }
634
618
  function positiveIntAttr(element, name) {
635
619
  const raw = require_xml_query.attrValue(element, name);
@@ -33,6 +33,12 @@ function withQuote(state) {
33
33
  quoteDepth: state.quoteDepth + 1
34
34
  };
35
35
  }
36
+ function withExtraIndent(state, pt) {
37
+ return {
38
+ ...state,
39
+ extraIndentPt: state.extraIndentPt + pt
40
+ };
41
+ }
36
42
  function withListItem(state, listItem) {
37
43
  return {
38
44
  ...state,
@@ -47,9 +53,13 @@ function decorateParagraph(paragraph, state) {
47
53
  let decorated = paragraph;
48
54
  if (state.quoteDepth > 0) decorated = {
49
55
  ...decorated,
50
- indentLeftPt: state.quoteDepth * 36,
56
+ indentLeftPt: state.quoteDepth * 36 + state.extraIndentPt,
51
57
  styleId: decorated.styleId ?? "Quote"
52
58
  };
59
+ else if (state.extraIndentPt > 0) decorated = {
60
+ ...decorated,
61
+ indentLeftPt: state.extraIndentPt
62
+ };
53
63
  if (state.listItem !== void 0) {
54
64
  const membership = {
55
65
  numId: state.listItem.numId,
@@ -131,6 +141,7 @@ function readXhtmlBody(xml, options) {
131
141
  context,
132
142
  minter: createIdMinter(),
133
143
  quoteDepth: 0,
144
+ extraIndentPt: 0,
134
145
  listItem: void 0,
135
146
  list: void 0,
136
147
  contentWidthPt: options.contentWidthPt
@@ -164,12 +175,12 @@ const BLOCK_LEVEL_TAGS = /* @__PURE__ */ new Set([
164
175
  "nav",
165
176
  "img"
166
177
  ]);
167
- function readContainerChildren(nodes, state) {
178
+ function readContainerChildren(nodes, state, baseStyle = {}) {
168
179
  const blocks = [];
169
180
  let segment = [];
170
181
  const flush = () => {
171
182
  if (segment.length === 0) return;
172
- const inline = buildInlineRuns(segment, {}, state.context);
183
+ const inline = buildInlineRuns(segment, baseStyle, state.context);
173
184
  segment = [];
174
185
  if (inline.runs.every((run) => run.text.trim().length === 0) && inline.constructs.length === 0) return;
175
186
  const paragraph = {
@@ -237,14 +248,7 @@ function readBlockElementInner(element, state) {
237
248
  case "dl": return readDefinitionList(element, state);
238
249
  case "table": return readTable(element, state);
239
250
  case "figure": return readContainerChildren(element.children, state);
240
- case "figcaption": {
241
- const inline = buildInlineRuns(element.children, {}, state.context);
242
- return [decorateParagraph({
243
- kind: "paragraph",
244
- runs: inline.runs,
245
- ...constructsField(inline)
246
- }, state)];
247
- }
251
+ case "figcaption": return readContainerChildren(element.children, state);
248
252
  case "img": {
249
253
  const block = readImage(element, state);
250
254
  return block === void 0 ? [] : [block];
@@ -449,23 +453,12 @@ function readDefinitionListEntries(nodes, state) {
449
453
  for (const child of nodes) {
450
454
  if (child.type === "element" && child.tag === "dt") {
451
455
  flushStray();
452
- const inline = buildInlineRuns(child.children, {}, state.context);
453
- blocks.push(decorateParagraph({
454
- kind: "paragraph",
455
- runs: inline.runs,
456
- ...constructsField(inline)
457
- }, state));
456
+ blocks.push(...readContainerChildren(child.children, state));
458
457
  continue;
459
458
  }
460
459
  if (child.type === "element" && child.tag === "dd") {
461
460
  flushStray();
462
- const inline = buildInlineRuns(child.children, {}, state.context);
463
- blocks.push(decorateParagraph({
464
- kind: "paragraph",
465
- runs: inline.runs,
466
- ...constructsField(inline),
467
- indentLeftPt: 36 + state.quoteDepth * 36
468
- }, state));
461
+ blocks.push(...readContainerChildren(child.children, withExtraIndent(state, 36)));
469
462
  continue;
470
463
  }
471
464
  if (child.type === "element" && child.tag === "div") {
@@ -538,16 +531,11 @@ function readTable(element, state) {
538
531
  if (cellNode.type === "element" && (cellNode.tag === "td" || cellNode.tag === "th")) {
539
532
  flushStrayCell();
540
533
  const cellStyle = cellNode.tag === "th" ? { bold: true } : {};
541
- const inline = buildInlineRuns(cellNode.children, cellStyle, state.context);
542
- const paragraph = {
543
- kind: "paragraph",
544
- runs: inline.runs,
545
- ...constructsField(inline)
546
- };
534
+ const cellBlocks = readContainerChildren(cellNode.children, state, cellStyle);
547
535
  const colSpan = positiveIntAttr(cellNode, "colspan");
548
536
  const rowSpan = positiveIntAttr(cellNode, "rowspan");
549
537
  cells.push({
550
- blocks: [paragraph],
538
+ blocks: cellBlocks,
551
539
  ...colSpan !== void 0 ? { colSpan } : {},
552
540
  ...rowSpan !== void 0 ? { rowSpan } : {}
553
541
  });
@@ -610,8 +598,8 @@ function collectColgroupStrayContent(section, strayNodes, context) {
610
598
  }
611
599
  }
612
600
  function readTableCaption(captionElement, isDuplicate, state) {
613
- const captionInline = buildInlineRuns(captionElement.children, {}, state.context);
614
- if (captionInline.runs.every((run) => run.text.trim().length === 0) && captionInline.constructs.length === 0) return [];
601
+ const captionBlocks = readContainerChildren(captionElement.children, state);
602
+ if (captionBlocks.length === 0) return [];
615
603
  if (isDuplicate) state.context.sink({
616
604
  code: EpubDiagnosticCodes.TABLE_DUPLICATE_CAPTION,
617
605
  severity: "info",
@@ -624,11 +612,7 @@ function readTableCaption(captionElement, isDuplicate, state) {
624
612
  message: "<caption> has no document-schema.js table-caption field to carry its own distinct tag; read as an ordinary paragraph immediately before the table",
625
613
  href: state.context.sourceHref
626
614
  });
627
- return [decorateParagraph({
628
- kind: "paragraph",
629
- runs: captionInline.runs,
630
- ...constructsField(captionInline)
631
- }, state)];
615
+ return captionBlocks;
632
616
  }
633
617
  function positiveIntAttr(element, name) {
634
618
  const raw = attrValue(element, name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epub-codec",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "EPUB 2/3 reading and deterministic EPUB 3 writing against the shared document-schema.js content pivot.",
5
5
  "type": "module",
6
6
  "repository": {