@voidberg/quarto 0.1.0

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/dist/html.js ADDED
@@ -0,0 +1,430 @@
1
+ import { parse, parseFragment } from "parse5";
2
+ const VOID_ELEMENTS = new Set([
3
+ "area",
4
+ "base",
5
+ "br",
6
+ "col",
7
+ "embed",
8
+ "hr",
9
+ "img",
10
+ "input",
11
+ "link",
12
+ "meta",
13
+ "param",
14
+ "source",
15
+ "track",
16
+ "wbr",
17
+ ]);
18
+ // Inline elements whose content model is phrasing-only: they may not contain
19
+ // block/flow elements. (`a`, `ins`, `del`, `map` are transparent — excluded.)
20
+ const PHRASING_ONLY = new Set([
21
+ "span",
22
+ "em",
23
+ "strong",
24
+ "b",
25
+ "i",
26
+ "u",
27
+ "s",
28
+ "small",
29
+ "mark",
30
+ "code",
31
+ "kbd",
32
+ "samp",
33
+ "sub",
34
+ "sup",
35
+ "abbr",
36
+ "cite",
37
+ "q",
38
+ "dfn",
39
+ "time",
40
+ "var",
41
+ "bdi",
42
+ "bdo",
43
+ "big",
44
+ "tt",
45
+ ]);
46
+ // Block/flow elements that may not appear inside phrasing content.
47
+ const BLOCK_ELEMENTS = new Set([
48
+ "div",
49
+ "p",
50
+ "section",
51
+ "article",
52
+ "aside",
53
+ "header",
54
+ "footer",
55
+ "nav",
56
+ "main",
57
+ "figure",
58
+ "figcaption",
59
+ "blockquote",
60
+ "ul",
61
+ "ol",
62
+ "li",
63
+ "dl",
64
+ "dt",
65
+ "dd",
66
+ "table",
67
+ "caption",
68
+ "thead",
69
+ "tbody",
70
+ "tfoot",
71
+ "tr",
72
+ "td",
73
+ "th",
74
+ "h1",
75
+ "h2",
76
+ "h3",
77
+ "h4",
78
+ "h5",
79
+ "h6",
80
+ "hr",
81
+ "pre",
82
+ "form",
83
+ "fieldset",
84
+ "address",
85
+ ]);
86
+ function isText(node) {
87
+ return node.nodeName === "#text";
88
+ }
89
+ function isComment(node) {
90
+ return node.nodeName === "#comment";
91
+ }
92
+ function isElement(node) {
93
+ return "tagName" in node && Array.isArray(node.attrs);
94
+ }
95
+ function escapeText(value) {
96
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
97
+ }
98
+ function escapeAttr(value) {
99
+ // Only &, < and " need escaping in a double-quoted XML attribute value;
100
+ // regular spaces are valid and must be preserved (so e.g. multi-token class
101
+ // names and alt/title text survive intact).
102
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
103
+ }
104
+ /** Serialize a parse5 node list as well-formed XHTML. */
105
+ function serializeNodes(nodes) {
106
+ let out = "";
107
+ for (const node of nodes) {
108
+ if (isText(node)) {
109
+ out += escapeText(node.value);
110
+ }
111
+ else if (isComment(node)) {
112
+ out += `<!--${node.data}-->`;
113
+ }
114
+ else if (isElement(node)) {
115
+ const tag = node.tagName;
116
+ const attrs = node.attrs
117
+ .map((a) => {
118
+ const name = a.prefix ? `${a.prefix}:${a.name}` : a.name;
119
+ return ` ${name}="${escapeAttr(a.value)}"`;
120
+ })
121
+ .join("");
122
+ if (VOID_ELEMENTS.has(tag) && node.childNodes.length === 0) {
123
+ out += `<${tag}${attrs}/>`;
124
+ }
125
+ else {
126
+ out += `<${tag}${attrs}>${serializeNodes(node.childNodes)}</${tag}>`;
127
+ }
128
+ }
129
+ }
130
+ return out;
131
+ }
132
+ /** Walk every element in a subtree, depth-first. */
133
+ function walkElements(nodes, visit) {
134
+ for (const node of nodes) {
135
+ if (isElement(node)) {
136
+ visit(node);
137
+ walkElements(node.childNodes, visit);
138
+ }
139
+ }
140
+ }
141
+ /**
142
+ * Browsers (and parse5) tolerate block elements nested inside phrasing-only
143
+ * inline elements like `<span><div>…</div></span>`, but that violates the XHTML
144
+ * content model and EPUBCheck rejects it. Bottom-up, rewrite any phrasing-only
145
+ * element that contains a block child into a `<div>` (keeping its attributes), so
146
+ * the structure becomes valid without losing content.
147
+ */
148
+ function fixBlockInPhrasing(nodes) {
149
+ for (const node of nodes) {
150
+ if (!isElement(node))
151
+ continue;
152
+ fixBlockInPhrasing(node.childNodes);
153
+ if (PHRASING_ONLY.has(node.tagName) &&
154
+ node.childNodes.some((c) => isElement(c) && BLOCK_ELEMENTS.has(c.tagName))) {
155
+ node.nodeName = "div";
156
+ node.tagName = "div";
157
+ }
158
+ }
159
+ }
160
+ function getAttr(el, name) {
161
+ return el.attrs.find((a) => a.name === name)?.value;
162
+ }
163
+ function setAttr(el, name, value) {
164
+ const existing = el.attrs.find((a) => a.name === name);
165
+ if (existing)
166
+ existing.value = value;
167
+ else
168
+ el.attrs.push({ name, value });
169
+ }
170
+ /** A parsed HTML fragment ready to inspect, mutate and serialize. */
171
+ export class HtmlFragment {
172
+ nodes;
173
+ constructor(nodes) {
174
+ this.nodes = nodes;
175
+ }
176
+ static parse(html) {
177
+ const frag = parseFragment(html);
178
+ fixBlockInPhrasing(frag.childNodes);
179
+ return new HtmlFragment(frag.childNodes);
180
+ }
181
+ /** Collect the `src` of every `<img>` (in document order). */
182
+ imageSources() {
183
+ const srcs = [];
184
+ walkElements(this.nodes, (el) => {
185
+ if (el.tagName === "img") {
186
+ const src = getAttr(el, "src");
187
+ if (src)
188
+ srcs.push(src);
189
+ }
190
+ });
191
+ return srcs;
192
+ }
193
+ /**
194
+ * Walk every `<img>` (depth-first, splice-safe) and apply a decision based on
195
+ * its `src`:
196
+ * - a string → rewrite `src` to it (and ensure `alt`);
197
+ * - `null` → remove the element;
198
+ * - `undefined` → leave it (and ensure `alt`).
199
+ */
200
+ transformImages(decide) {
201
+ const process = (nodes) => {
202
+ for (let i = nodes.length - 1; i >= 0; i--) {
203
+ const node = nodes[i];
204
+ if (!isElement(node))
205
+ continue;
206
+ if (node.tagName === "img") {
207
+ const result = decide(getAttr(node, "src"));
208
+ if (result === null) {
209
+ nodes.splice(i, 1);
210
+ }
211
+ else {
212
+ if (typeof result === "string")
213
+ setAttr(node, "src", result);
214
+ if (getAttr(node, "alt") === undefined)
215
+ setAttr(node, "alt", "");
216
+ }
217
+ }
218
+ else {
219
+ process(node.childNodes);
220
+ }
221
+ }
222
+ };
223
+ process(this.nodes);
224
+ }
225
+ /**
226
+ * Resolve every `<img>` against an embedded-image map (original src → in-package
227
+ * path). Matched images are rewritten and given alt text. When
228
+ * `stripUnresolved` is set, images that were not embedded (failed downloads,
229
+ * unsupported sources) are removed entirely so the EPUB has no dangling
230
+ * foreign resources — EPUBCheck rejects those.
231
+ */
232
+ resolveImages(map, stripUnresolved) {
233
+ this.transformImages((src) => {
234
+ if (src && map.has(src))
235
+ return map.get(src);
236
+ return stripUnresolved ? null : undefined;
237
+ });
238
+ }
239
+ /**
240
+ * Remove every `<img>` that references something outside the container — i.e.
241
+ * anything that isn't an inline `data:` URI (remote URLs and relative paths
242
+ * alike). Used when image downloading is disabled: only self-contained `data:`
243
+ * images can survive, so this keeps the EPUB valid instead of leaving dangling
244
+ * references EPUBCheck rejects.
245
+ */
246
+ stripExternalImages() {
247
+ this.transformImages((src) => (src && /^data:/i.test(src) ? undefined : null));
248
+ }
249
+ /**
250
+ * If the fragment's first meaningful content is an image (i.e. no text
251
+ * precedes it), remove that image and return its `src` — for promoting an
252
+ * article's lead image to the cover. Returns `undefined` when text comes first,
253
+ * so we never pull an image out of the middle of the prose.
254
+ */
255
+ extractLeadImage() {
256
+ let src;
257
+ // A wrapper is "empty" if nothing but whitespace/comments remains.
258
+ const isEmpty = (el) => el.childNodes.every((c) => isComment(c) || (isText(c) && c.value.trim() === ""));
259
+ const visit = (nodes) => {
260
+ for (let i = 0; i < nodes.length; i++) {
261
+ const node = nodes[i];
262
+ if (isText(node)) {
263
+ if (node.value.trim() !== "")
264
+ return "text";
265
+ continue;
266
+ }
267
+ if (!isElement(node))
268
+ continue;
269
+ if (node.tagName === "img") {
270
+ src = getAttr(node, "src");
271
+ nodes.splice(i, 1);
272
+ return "img";
273
+ }
274
+ const inner = visit(node.childNodes);
275
+ if (inner === "img") {
276
+ // The lead image lived inside this wrapper; drop it too if now empty
277
+ // (a remaining <figcaption> etc. keeps it). Propagates up the nesting.
278
+ if (isEmpty(node))
279
+ nodes.splice(i, 1);
280
+ return "img";
281
+ }
282
+ if (inner === "text")
283
+ return "text";
284
+ }
285
+ return undefined;
286
+ };
287
+ return visit(this.nodes) === "img" ? src : undefined;
288
+ }
289
+ serialize() {
290
+ return serializeNodes(this.nodes);
291
+ }
292
+ }
293
+ // --- kepub transform ---------------------------------------------------------
294
+ // Block-level tags whose text Kobo expects to be segmented for location tracking.
295
+ // (`pre` is intentionally absent — it lives in KOBO_SKIP, whose check runs first.)
296
+ const KOBO_SEGMENT_BLOCKS = new Set([
297
+ "p",
298
+ "li",
299
+ "h1",
300
+ "h2",
301
+ "h3",
302
+ "h4",
303
+ "h5",
304
+ "h6",
305
+ "blockquote",
306
+ "td",
307
+ "th",
308
+ "figcaption",
309
+ "dd",
310
+ "dt",
311
+ ]);
312
+ // Never wrap text inside these.
313
+ const KOBO_SKIP = new Set(["script", "style", "pre", "audio", "video", "svg", "math"]);
314
+ /**
315
+ * Transform an XHTML content document into its Kobo (kepub) form, mirroring
316
+ * kepubify:
317
+ *
318
+ * - every text run inside a block element is wrapped in
319
+ * `<span class="koboSpan" id="kobo.{segment}.{fragment}">` so the firmware can
320
+ * track reading position and anchor highlights precisely;
321
+ * - the body content is wrapped in `<div id="book-columns"><div id="book-inner">`
322
+ * which Kobo relies on for pagination and justification.
323
+ *
324
+ * Returns a complete XHTML document string. If the input has no parseable
325
+ * `<body>`, it is returned unchanged.
326
+ */
327
+ export function kepubifyXhtml(documentHtml) {
328
+ const doc = parse(documentHtml);
329
+ const html = findElement(doc.childNodes, "html");
330
+ const body = html ? findElement(html.childNodes, "body") : undefined;
331
+ if (!html || !body)
332
+ return documentHtml;
333
+ const counter = { segment: 0 };
334
+ wrapTextNodes(body.childNodes, counter);
335
+ const inner = {
336
+ nodeName: "div",
337
+ tagName: "div",
338
+ attrs: [{ name: "id", value: "book-inner" }],
339
+ childNodes: body.childNodes,
340
+ };
341
+ const columns = {
342
+ nodeName: "div",
343
+ tagName: "div",
344
+ attrs: [{ name: "id", value: "book-columns" }],
345
+ childNodes: [inner],
346
+ };
347
+ body.childNodes = [columns];
348
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE html>\n${serializeNodes([html])}\n`;
349
+ }
350
+ function findElement(nodes, tagName) {
351
+ for (const node of nodes) {
352
+ if (isElement(node)) {
353
+ if (node.tagName === tagName)
354
+ return node;
355
+ const nested = findElement(node.childNodes, tagName);
356
+ if (nested)
357
+ return nested;
358
+ }
359
+ }
360
+ return undefined;
361
+ }
362
+ /**
363
+ * Recurse into the tree, treating each block in {@link KOBO_SEGMENT_BLOCKS} as
364
+ * one location segment and wrapping its text via {@link wrapInline}.
365
+ *
366
+ * Known simplification vs. full kepubify: only text inside a recognised block
367
+ * is wrapped. Loose text that sits directly in a non-block container (e.g. a
368
+ * bare text node under a `<div>`) is left unwrapped, and a block nested inside
369
+ * another block shares its parent's segment number. Content that quarto itself
370
+ * generates is always block-wrapped, so this only affects arbitrary inputs and
371
+ * costs Kobo a little location-tracking granularity — never validity.
372
+ */
373
+ function wrapTextNodes(nodes, counter) {
374
+ for (let i = 0; i < nodes.length; i++) {
375
+ const node = nodes[i];
376
+ if (isElement(node)) {
377
+ if (KOBO_SKIP.has(node.tagName))
378
+ continue;
379
+ if (KOBO_SEGMENT_BLOCKS.has(node.tagName)) {
380
+ counter.segment++;
381
+ wrapInline(node, counter.segment);
382
+ }
383
+ else {
384
+ wrapTextNodes(node.childNodes, counter);
385
+ }
386
+ }
387
+ }
388
+ }
389
+ /**
390
+ * Wrap each non-empty text node within a block in its own koboSpan, numbering
391
+ * fragments sequentially. Nested inline elements keep their structure; only the
392
+ * text leaves are wrapped.
393
+ */
394
+ function wrapInline(block, segment) {
395
+ const fragment = { n: 0 };
396
+ const transform = (nodes) => {
397
+ const result = [];
398
+ for (const node of nodes) {
399
+ if (isText(node)) {
400
+ if (node.value.trim() === "") {
401
+ result.push(node);
402
+ continue;
403
+ }
404
+ fragment.n++;
405
+ result.push(koboSpan(segment, fragment.n, [node]));
406
+ }
407
+ else if (isElement(node) && !KOBO_SKIP.has(node.tagName)) {
408
+ node.childNodes = transform(node.childNodes);
409
+ result.push(node);
410
+ }
411
+ else {
412
+ result.push(node);
413
+ }
414
+ }
415
+ return result;
416
+ };
417
+ block.childNodes = transform(block.childNodes);
418
+ }
419
+ function koboSpan(segment, fragment, children) {
420
+ return {
421
+ nodeName: "span",
422
+ tagName: "span",
423
+ attrs: [
424
+ { name: "class", value: "koboSpan" },
425
+ { name: "id", value: `kobo.${segment}.${fragment}` },
426
+ ],
427
+ childNodes: children,
428
+ };
429
+ }
430
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { ImageTransform } from "./types.js";
2
+ export interface EmbeddedImage {
3
+ /** Path inside the EPUB, e.g. `images/img-1.jpg`. */
4
+ path: string;
5
+ /** Manifest id. */
6
+ id: string;
7
+ mime: string;
8
+ data: Uint8Array;
9
+ }
10
+ /** Fetch a single image URL (or accept raw bytes) and classify its type. */
11
+ export declare function fetchImage(source: string | Uint8Array, fetcher: typeof fetch): Promise<{
12
+ data: Uint8Array;
13
+ mime: string;
14
+ }>;
15
+ /**
16
+ * Download a set of image sources concurrently, skipping any that fail, return
17
+ * no bytes, or aren't an EPUB core image type (the book stays valid even when a
18
+ * source 404s or is unsupported). Returns the embedded images plus a map from
19
+ * original URL → in-package path for rewriting.
20
+ */
21
+ export declare function embedImages(urls: string[], fetcher: typeof fetch, transform?: ImageTransform): Promise<{
22
+ images: EmbeddedImage[];
23
+ rewrites: Map<string, string>;
24
+ }>;
package/dist/images.js ADDED
@@ -0,0 +1,51 @@
1
+ import { extFromMime, isCoreImageType, mimeFromUrl, sniffImageMime } from "./util.js";
2
+ /** Fetch a single image URL (or accept raw bytes) and classify its type. */
3
+ export async function fetchImage(source, fetcher) {
4
+ if (typeof source !== "string") {
5
+ return { data: source, mime: sniffImageMime(source) ?? "image/jpeg" };
6
+ }
7
+ const res = await fetcher(source);
8
+ if (!res.ok)
9
+ throw new Error(`Failed to fetch image ${source}: HTTP ${res.status}`);
10
+ const data = new Uint8Array(await res.arrayBuffer());
11
+ const headerMime = res.headers.get("content-type")?.split(";")[0]?.trim();
12
+ const mime = sniffImageMime(data) ||
13
+ (headerMime?.startsWith("image/") ? headerMime : undefined) ||
14
+ mimeFromUrl(source) ||
15
+ "image/jpeg";
16
+ return { data, mime };
17
+ }
18
+ /**
19
+ * Download a set of image sources concurrently, skipping any that fail, return
20
+ * no bytes, or aren't an EPUB core image type (the book stays valid even when a
21
+ * source 404s or is unsupported). Returns the embedded images plus a map from
22
+ * original URL → in-package path for rewriting.
23
+ */
24
+ export async function embedImages(urls, fetcher, transform) {
25
+ const unique = [...new Set(urls)].filter((u) => u.length > 0);
26
+ const images = [];
27
+ const rewrites = new Map();
28
+ const results = await Promise.allSettled(unique.map(async (url) => {
29
+ const img = await fetchImage(url, fetcher);
30
+ return transform ? await transform(img) : img;
31
+ }));
32
+ let counter = 0;
33
+ results.forEach((result, i) => {
34
+ const url = unique[i];
35
+ // Skip anything we can't embed: failed fetches, transform-dropped (null) or
36
+ // empty payloads, and types EPUB can't carry without a fallback — the
37
+ // corresponding <img> is then stripped so the EPUB stays valid.
38
+ if (result.status !== "fulfilled" || result.value === null)
39
+ return;
40
+ if (result.value.data.length === 0 || !isCoreImageType(result.value.mime))
41
+ return;
42
+ counter++;
43
+ const ext = extFromMime(result.value.mime);
44
+ const path = `images/img-${counter}.${ext}`;
45
+ const id = `img-${counter}`;
46
+ images.push({ path, id, mime: result.value.mime, data: result.value.data });
47
+ rewrites.set(url, path);
48
+ });
49
+ return { images, rewrites };
50
+ }
51
+ //# sourceMappingURL=images.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"images.js","sourceRoot":"","sources":["../src/images.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { DEFAULT_CSS } from "./css.js";
2
+ export { generateEpub } from "./epub.js";
3
+ export { toKepub } from "./kepub.js";
4
+ export type { Chapter, CoverTransform, EpubInput, EpubOptions, ImageSource, ImageTransform, RawImage, } from "./types.js";
5
+ export { imageSize } from "./util.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { DEFAULT_CSS } from "./css.js";
2
+ export { generateEpub } from "./epub.js";
3
+ export { toKepub } from "./kepub.js";
4
+ export { imageSize } from "./util.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAUrC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Convert a standard EPUB into a Kobo kepub in memory — the equivalent of
3
+ * running `kepubify`, with no external binary required.
4
+ *
5
+ * Every content document is rewritten with Kobo reading-location spans and the
6
+ * `book-columns`/`book-inner` wrappers. The navigation document is left
7
+ * untouched. The returned bytes should be written with a `.kepub.epub`
8
+ * extension so Kobo devices recognise the enhanced format.
9
+ */
10
+ export declare function toKepub(epub: Uint8Array): Uint8Array;
package/dist/kepub.js ADDED
@@ -0,0 +1,32 @@
1
+ import { kepubifyXhtml } from "./html.js";
2
+ import { decodeUtf8, encodeUtf8 } from "./util.js";
3
+ import { unzipEpub, zipEpub } from "./zip.js";
4
+ const XHTML_RE = /\.x?html?$/i;
5
+ const NAV_RE = /epub:type=["']toc["']/;
6
+ /**
7
+ * Convert a standard EPUB into a Kobo kepub in memory — the equivalent of
8
+ * running `kepubify`, with no external binary required.
9
+ *
10
+ * Every content document is rewritten with Kobo reading-location spans and the
11
+ * `book-columns`/`book-inner` wrappers. The navigation document is left
12
+ * untouched. The returned bytes should be written with a `.kepub.epub`
13
+ * extension so Kobo devices recognise the enhanced format.
14
+ */
15
+ export function toKepub(epub) {
16
+ const entries = unzipEpub(epub);
17
+ const files = {};
18
+ for (const [path, bytes] of Object.entries(entries)) {
19
+ if (path === "mimetype")
20
+ continue; // re-added (stored, first) by zipEpub
21
+ if (XHTML_RE.test(path)) {
22
+ const html = decodeUtf8(bytes);
23
+ // Skip the navigation document; Kobo builds its TOC from it directly.
24
+ files[path] = NAV_RE.test(html) ? bytes : encodeUtf8(kepubifyXhtml(html));
25
+ }
26
+ else {
27
+ files[path] = bytes;
28
+ }
29
+ }
30
+ return zipEpub(files);
31
+ }
32
+ //# sourceMappingURL=kepub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kepub.js","sourceRoot":"","sources":["../src/kepub.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAE9C,MAAM,QAAQ,GAAG,aAAa,CAAC;AAC/B,MAAM,MAAM,GAAG,uBAAuB,CAAC;AAEvC;;;;;;;;GAQG;AACH,MAAM,UAAU,OAAO,CAAC,IAAgB;IACtC,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,KAAK,GAA+B,EAAE,CAAC;IAE7C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,IAAI,IAAI,KAAK,UAAU;YAAE,SAAS,CAAC,sCAAsC;QACzE,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;YAC/B,sEAAsE;YACtE,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5E,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACtB,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;AACxB,CAAC"}
package/dist/opf.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ export interface ManifestItem {
2
+ id: string;
3
+ href: string;
4
+ mediaType: string;
5
+ /** EPUB3 manifest properties, e.g. `"nav"` or `"cover-image"`. */
6
+ properties?: string;
7
+ }
8
+ export interface NavEntry {
9
+ href: string;
10
+ title: string;
11
+ }
12
+ /** A reference for the EPUB2 `<guide>` or the EPUB3 landmarks nav (e.g. cover, start). */
13
+ export interface Reference {
14
+ type: string;
15
+ href: string;
16
+ title: string;
17
+ }
18
+ export interface OpfMetadata {
19
+ id: string;
20
+ title: string;
21
+ authors: string[];
22
+ language: string;
23
+ publisher?: string;
24
+ description?: string;
25
+ date?: string;
26
+ coverImageId?: string;
27
+ }
28
+ export declare const CONTAINER_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n <rootfiles>\n <rootfile full-path=\"EPUB/content.opf\" media-type=\"application/oebps-package+xml\"/>\n </rootfiles>\n</container>\n";
29
+ export declare function buildOpf(meta: OpfMetadata, manifest: ManifestItem[], spine: string[], options?: {
30
+ ncxId?: string;
31
+ guide?: Reference[];
32
+ }): string;
33
+ /**
34
+ * EPUB3 navigation document. EPUB3 mandates exactly one `nav` document, so even
35
+ * when a book opts out of a table of contents we still emit one — but mark it
36
+ * `hidden` and keep it out of the spine so no visible TOC page appears.
37
+ */
38
+ export declare function buildNav(navTitle: string, language: string, entries: NavEntry[], hidden?: boolean, landmarks?: Reference[]): string;
39
+ /** EPUB2 NCX, kept for older e-reader compatibility. */
40
+ export declare function buildNcx(id: string, title: string, entries: NavEntry[]): string;
41
+ export interface SvgCoverOptions {
42
+ title: string;
43
+ language: string;
44
+ imageHref: string;
45
+ width: number;
46
+ height: number;
47
+ /** Fills the letterbox bands around the cover image (e.g. to match its edges). */
48
+ background?: string;
49
+ }
50
+ /**
51
+ * A cover page that wraps the image in an SVG scaled to fit the viewport
52
+ * (`preserveAspectRatio="xMidYMid meet"`). This is the portable way to make a
53
+ * cover fill and center consistently across readers — inline `<img>` layout
54
+ * anchors awkwardly on e-ink devices like Kobo.
55
+ */
56
+ export declare function buildSvgCover(opts: SvgCoverOptions): string;
57
+ export interface XhtmlPageOptions {
58
+ title: string;
59
+ language: string;
60
+ cssHref?: string;
61
+ bodyHtml: string;
62
+ bodyClass?: string;
63
+ }
64
+ /** Wrap body HTML in a complete XHTML content document. */
65
+ export declare function buildXhtml(opts: XhtmlPageOptions): string;