@papyrus-sdk/engine-epub 0.2.6 → 0.2.7

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/index.js CHANGED
@@ -34,17 +34,48 @@ __export(engine_epub_exports, {
34
34
  module.exports = __toCommonJS(engine_epub_exports);
35
35
  var import_epubjs = __toESM(require("epubjs"));
36
36
  var import_core = require("@papyrus-sdk/core");
37
- var EPUBEngine = class extends import_core.BaseDocumentEngine {
37
+ var EPUBEngine = class _EPUBEngine extends import_core.BaseDocumentEngine {
38
38
  constructor() {
39
39
  super(...arguments);
40
40
  this.book = null;
41
41
  this.spineItems = [];
42
- this.renditions = /* @__PURE__ */ new Map();
43
- this.renditionTargets = /* @__PURE__ */ new Map();
42
+ this.coverUrl = null;
43
+ this.readerRendition = null;
44
+ this.readerTarget = null;
44
45
  this.pageSizes = /* @__PURE__ */ new Map();
45
46
  this.currentPage = 1;
46
47
  this.zoom = 1;
47
48
  this.rotation = 0;
49
+ this.heightSyncVersion = 0;
50
+ this.renderVersion = 0;
51
+ this.renderLock = Promise.resolve();
52
+ this.pendingHrefDestination = null;
53
+ this.destinationSequence = [];
54
+ this.destinationCursor = -1;
55
+ this.lastDestinationNavTime = 0;
56
+ this.lastDestinationPageIndex = null;
57
+ this.lastDestinationHref = null;
58
+ }
59
+ static {
60
+ this.A4_RATIO = 1.4142;
61
+ }
62
+ static {
63
+ this.USE_INTERNAL_IFRAME_SCROLL = true;
64
+ }
65
+ static {
66
+ this.MOBILE_VIEWPORT_MAX_WIDTH_PX = 768;
67
+ }
68
+ static {
69
+ this.MOBILE_SHORT_VIEWPORT_MAX_HEIGHT_PX = 500;
70
+ }
71
+ static {
72
+ this.INTERNAL_VIEWPORT_PADDING_PX = 10;
73
+ }
74
+ static {
75
+ this.MAX_SECTION_HEIGHT = 1e6;
76
+ }
77
+ static {
78
+ this.HEIGHT_PADDING = 24;
48
79
  }
49
80
  getRenderTargetType() {
50
81
  return "element";
@@ -53,34 +84,243 @@ var EPUBEngine = class extends import_core.BaseDocumentEngine {
53
84
  try {
54
85
  const { source, type } = this.normalizeLoadInput(input);
55
86
  if (type && type !== "epub") {
56
- throw new Error(`[EPUBEngine] Tipo de documento n\xE3o suportado: ${type}`);
87
+ throw new Error(
88
+ `[EPUBEngine] Tipo de documento n\xE3o suportado: ${type}`
89
+ );
57
90
  }
91
+ this.renderVersion += 1;
92
+ this.renderLock = Promise.resolve();
93
+ this.disposeReader();
94
+ this.pageSizes.clear();
95
+ this.spineItems = [];
96
+ this.coverUrl = null;
97
+ this.destinationSequence = [];
98
+ this.destinationCursor = -1;
99
+ this.lastDestinationNavTime = 0;
100
+ this.lastDestinationPageIndex = null;
101
+ this.lastDestinationHref = null;
102
+ if (this.book?.destroy) this.book.destroy();
58
103
  const data = await this.resolveSource(source);
59
104
  this.book = (0, import_epubjs.default)(data);
105
+ if (this.book?.opened) {
106
+ await this.book.opened;
107
+ }
60
108
  await this.book.ready;
61
- this.spineItems = this.book.spine?.items ?? [];
109
+ const packaging = this.book?.package ?? this.book?.packaging ?? null;
110
+ if (!packaging) {
111
+ throw new Error("[EPUBEngine] packaging indisponivel.");
112
+ }
113
+ if (!this.book.package) {
114
+ this.book.package = packaging;
115
+ }
116
+ if (!this.book.packaging) {
117
+ this.book.packaging = packaging;
118
+ }
119
+ if (this.book?.loaded?.navigation) {
120
+ await this.book.loaded.navigation;
121
+ }
122
+ this.spineItems = this.getLinearSpineItems(this.book.spine?.items ?? []);
123
+ if (typeof this.book.coverUrl === "function") {
124
+ try {
125
+ const resolvedCover = await this.book.coverUrl();
126
+ if (typeof resolvedCover === "string" && resolvedCover.length > 0) {
127
+ this.coverUrl = resolvedCover;
128
+ }
129
+ } catch {
130
+ this.coverUrl = null;
131
+ }
132
+ }
62
133
  this.currentPage = 1;
63
- this.renditions.forEach((rendition) => rendition?.destroy?.());
64
- this.renditions.clear();
65
- this.renditionTargets.clear();
66
- this.pageSizes.clear();
67
134
  } catch (error) {
68
135
  console.error("[EPUBEngine] Erro ao carregar:", error);
69
136
  throw error;
70
137
  }
71
138
  }
72
139
  getPageCount() {
73
- return this.spineItems.length;
140
+ return this.spineItems.length + (this.hasCoverPage() ? 1 : 0);
74
141
  }
75
142
  getCurrentPage() {
76
143
  return this.currentPage;
77
144
  }
78
145
  goToPage(page) {
79
- if (page >= 1 && page <= this.getPageCount()) this.currentPage = page;
146
+ if (page < 1 || page > this.getPageCount()) return;
147
+ this.currentPage = page;
148
+ this.pendingHrefDestination = null;
149
+ if (this.destinationSequence.length) {
150
+ const destinationIndex = this.findNearestDestinationIndexByPage(page - 1);
151
+ if (destinationIndex >= 0) this.destinationCursor = destinationIndex;
152
+ }
153
+ }
154
+ async goToDestination(dest) {
155
+ const href = this.extractHrefDestination(dest);
156
+ if (!href) {
157
+ const pageIndex = await this.getPageIndex(dest);
158
+ if (pageIndex != null) this.goToPage(pageIndex + 1);
159
+ return pageIndex;
160
+ }
161
+ const baseHref = this.normalizeHref(href);
162
+ const currentBaseHref = this.lastDestinationHref ? this.normalizeHref(this.lastDestinationHref) : null;
163
+ const hasAnchor = href.includes("#");
164
+ if (!hasAnchor && currentBaseHref && baseHref === currentBaseHref && this.readerRendition) {
165
+ this.debugLog("goToDestination:skip-same-base", {
166
+ href,
167
+ currentBaseHref
168
+ });
169
+ const pageIndex = await this.resolvePageIndexFromHref(href);
170
+ return pageIndex;
171
+ }
172
+ let releaseCurrent = null;
173
+ const previousRender = this.renderLock;
174
+ this.renderLock = new Promise((resolve) => {
175
+ releaseCurrent = resolve;
176
+ });
177
+ await previousRender;
178
+ try {
179
+ this.pendingHrefDestination = href;
180
+ let pageIndex = await this.resolvePageIndexFromHref(href);
181
+ if (pageIndex != null) this.currentPage = pageIndex + 1;
182
+ this.debugLog("goToDestination:start", {
183
+ href,
184
+ pageIndex,
185
+ currentPage: this.currentPage
186
+ });
187
+ const rendition = this.readerRendition;
188
+ let displayFailed = false;
189
+ if (rendition) {
190
+ try {
191
+ const didDisplay = await this.displayDestinationWithRetry(
192
+ rendition,
193
+ href
194
+ );
195
+ if (!didDisplay)
196
+ throw new Error("Destination display retry exhausted");
197
+ if (pageIndex == null) {
198
+ const locationPageIndex = this.getPageIndexFromRenditionLocation(rendition);
199
+ if (locationPageIndex != null) {
200
+ pageIndex = locationPageIndex;
201
+ this.currentPage = locationPageIndex + 1;
202
+ }
203
+ }
204
+ this.debugLog("goToDestination:displayed", {
205
+ href,
206
+ pageIndex,
207
+ currentPage: this.currentPage
208
+ });
209
+ this.updateDestinationCursor(href, pageIndex);
210
+ this.pendingHrefDestination = null;
211
+ this.lastDestinationNavTime = Date.now();
212
+ this.lastDestinationPageIndex = pageIndex;
213
+ this.lastDestinationHref = href;
214
+ return pageIndex;
215
+ } catch {
216
+ displayFailed = true;
217
+ this.debugLog("goToDestination:display-failed", { href, pageIndex });
218
+ }
219
+ }
220
+ if (displayFailed && pageIndex != null && this.readerTarget && typeof this.readerTarget.isConnected === "boolean" && this.readerTarget.isConnected) {
221
+ void this.renderPage(pageIndex, this.readerTarget, 1).catch(() => {
222
+ });
223
+ }
224
+ this.debugLog("goToDestination:no-rendition", {
225
+ href,
226
+ pageIndex,
227
+ currentPage: this.currentPage
228
+ });
229
+ this.updateDestinationCursor(href, pageIndex);
230
+ return pageIndex;
231
+ } finally {
232
+ releaseCurrent?.();
233
+ }
234
+ }
235
+ async goToAdjacentDestination(delta) {
236
+ if (!Number.isFinite(delta) || delta === 0) return this.currentPage - 1;
237
+ await this.ensureDestinationSequence();
238
+ if (!this.destinationSequence.length) {
239
+ const basePageIndex = Math.max(0, this.currentPage - 1);
240
+ const nextPageIndex = Math.max(
241
+ 0,
242
+ Math.min(this.getPageCount() - 1, basePageIndex + (delta > 0 ? 1 : -1))
243
+ );
244
+ if (nextPageIndex === basePageIndex) return basePageIndex;
245
+ this.goToPage(nextPageIndex + 1);
246
+ return nextPageIndex;
247
+ }
248
+ if (this.destinationCursor < 0) {
249
+ const inferred = this.findNearestDestinationIndexByPage(
250
+ this.currentPage - 1
251
+ );
252
+ this.destinationCursor = inferred >= 0 ? inferred : 0;
253
+ }
254
+ const currentEntry = this.destinationSequence[this.destinationCursor];
255
+ const currentBaseHref = currentEntry ? this.normalizeHref(currentEntry.href) : null;
256
+ this.debugLog("goToAdjacentDestination:start", {
257
+ delta,
258
+ cursor: this.destinationCursor,
259
+ total: this.destinationSequence.length,
260
+ currentPage: this.currentPage,
261
+ currentBaseHref
262
+ });
263
+ const step = delta > 0 ? 1 : -1;
264
+ let nextIndex = this.destinationCursor;
265
+ const limit = delta > 0 ? this.destinationSequence.length - 1 : 0;
266
+ while (true) {
267
+ const candidate = nextIndex + step;
268
+ if (candidate < 0 || candidate > this.destinationSequence.length - 1)
269
+ break;
270
+ nextIndex = candidate;
271
+ const candidateBaseHref = this.normalizeHref(
272
+ this.destinationSequence[nextIndex].href
273
+ );
274
+ if (!currentBaseHref || candidateBaseHref !== currentBaseHref) break;
275
+ }
276
+ if (nextIndex === this.destinationCursor) {
277
+ this.debugLog("goToAdjacentDestination:at-boundary", {
278
+ delta,
279
+ cursor: this.destinationCursor
280
+ });
281
+ const current = this.destinationSequence[this.destinationCursor];
282
+ return current?.pageIndex ?? this.currentPage - 1;
283
+ }
284
+ const target = this.destinationSequence[nextIndex];
285
+ if (!target) return null;
286
+ this.destinationCursor = nextIndex;
287
+ const resolved = await this.goToDestination({
288
+ kind: "href",
289
+ value: target.href
290
+ });
291
+ if (resolved != null) {
292
+ this.debugLog("goToAdjacentDestination:resolved", {
293
+ delta,
294
+ nextIndex,
295
+ href: target.href,
296
+ resolved
297
+ });
298
+ return resolved;
299
+ }
300
+ this.destinationCursor = nextIndex - step;
301
+ this.debugLog("goToAdjacentDestination:failed", {
302
+ delta,
303
+ nextIndex,
304
+ href: target.href
305
+ });
306
+ return null;
307
+ }
308
+ getDestinationNavigationState() {
309
+ const total = this.destinationSequence.length;
310
+ if (!total) {
311
+ return {
312
+ hasPrev: this.currentPage > 1,
313
+ hasNext: this.currentPage < this.getPageCount()
314
+ };
315
+ }
316
+ const cursor = this.destinationCursor >= 0 ? this.destinationCursor : this.findNearestDestinationIndexByPage(this.currentPage - 1);
317
+ return {
318
+ hasPrev: cursor > 0,
319
+ hasNext: cursor >= 0 && cursor < total - 1
320
+ };
80
321
  }
81
322
  setZoom(zoom) {
82
323
  this.zoom = Math.max(0.5, Math.min(3, zoom));
83
- this.renditions.forEach((rendition) => this.applyRenditionTheme(rendition));
84
324
  }
85
325
  getZoom() {
86
326
  return this.zoom;
@@ -104,45 +344,154 @@ var EPUBEngine = class extends import_core.BaseDocumentEngine {
104
344
  return null;
105
345
  }
106
346
  async renderPage(pageIndex, target, scale) {
107
- const scaleKey = Math.round(scale * 1e3);
108
- const element = target;
109
- if (!this.book || !element) return;
110
- const spineItem = this.spineItems[pageIndex];
111
- if (!spineItem) return;
112
- const width = element.clientWidth > 0 ? element.clientWidth : 640;
113
- const height = element.clientHeight > 0 ? element.clientHeight : 900;
114
- element.style.width = `${width}px`;
115
- element.style.height = `${height}px`;
116
- if (width >= 320 && height >= 480) this.pageSizes.set(pageIndex, { width, height });
117
- const renditionKey = `${pageIndex}:${scaleKey}`;
118
- let rendition = this.renditions.get(renditionKey);
119
- const currentTarget = this.renditionTargets.get(renditionKey);
120
- if (!rendition || currentTarget !== element) {
121
- if (rendition?.destroy) rendition.destroy();
122
- element.innerHTML = "";
123
- rendition = this.book.renderTo(element, {
124
- width,
125
- height,
126
- flow: "paginated",
127
- spread: "none"
128
- });
129
- this.renditions.set(renditionKey, rendition);
130
- this.renditionTargets.set(renditionKey, element);
131
- if (rendition?.hooks?.content?.register) {
132
- rendition.hooks.content.register((contents) => {
133
- const frame = contents?.document?.defaultView?.frameElement;
134
- if (frame) {
135
- frame.setAttribute("sandbox", "allow-scripts allow-same-origin");
347
+ let releaseCurrent = null;
348
+ const previousRender = this.renderLock;
349
+ this.renderLock = new Promise((resolve) => {
350
+ releaseCurrent = resolve;
351
+ });
352
+ await previousRender;
353
+ try {
354
+ const pageCount = this.getPageCount();
355
+ if (pageCount <= 0) return;
356
+ const safePageIndex = Math.max(0, Math.min(pageCount - 1, pageIndex));
357
+ const renderVersion = ++this.renderVersion;
358
+ const element = target;
359
+ if (!this.book || !element) return;
360
+ const width = element.clientWidth > 0 ? element.clientWidth : 640;
361
+ const viewportHeightHint = _EPUBEngine.USE_INTERNAL_IFRAME_SCROLL ? this.getViewportHeightHint(element) : null;
362
+ const rawHeight = viewportHeightHint ?? (element.clientHeight > 0 ? element.clientHeight : 900);
363
+ const normalizedHeight = _EPUBEngine.USE_INTERNAL_IFRAME_SCROLL ? Math.max(360, Math.min(4e3, rawHeight)) : Math.max(480, Math.min(1400, rawHeight));
364
+ const minA4Height = _EPUBEngine.USE_INTERNAL_IFRAME_SCROLL ? 0 : this.getA4MinHeight(width);
365
+ const seedHeight = _EPUBEngine.USE_INTERNAL_IFRAME_SCROLL ? normalizedHeight : Math.max(minA4Height, normalizedHeight);
366
+ if (this.isCoverPage(safePageIndex)) {
367
+ this.renderCoverPage(element, width, seedHeight, safePageIndex);
368
+ if (renderVersion === this.renderVersion) {
369
+ this.currentPage = safePageIndex + 1;
370
+ }
371
+ return;
372
+ }
373
+ const spineItem = this.getSpineItemForPage(safePageIndex);
374
+ if (!spineItem) return;
375
+ const displayTargets = [...this.getDisplayTargetsForSpineItem(spineItem)];
376
+ if (!displayTargets.length) return;
377
+ const defaultSectionIndex = typeof spineItem.index === "number" ? spineItem.index : null;
378
+ const pendingHrefDestination = this.pendingHrefDestination;
379
+ let consumedPendingHref = false;
380
+ if (pendingHrefDestination) {
381
+ const pendingPageIndex = await this.resolvePageIndexFromHref(
382
+ pendingHrefDestination
383
+ );
384
+ const pendingMatchesCurrentPage = pendingPageIndex === safePageIndex || this.isHrefMatchingSpineItem(pendingHrefDestination, spineItem);
385
+ if (pendingMatchesCurrentPage) {
386
+ consumedPendingHref = true;
387
+ displayTargets.unshift(
388
+ pendingHrefDestination,
389
+ this.decodeHref(pendingHrefDestination)
390
+ );
391
+ }
392
+ }
393
+ const uniqueTargets = this.uniqueDisplayTargets(displayTargets);
394
+ if (!uniqueTargets.length) return;
395
+ element.style.width = `${width}px`;
396
+ element.style.height = `${seedHeight}px`;
397
+ if (width >= 320 && seedHeight >= 480)
398
+ this.pageSizes.set(safePageIndex, { width, height: seedHeight });
399
+ let rendition = this.readerRendition;
400
+ if (!rendition || this.readerTarget !== element) {
401
+ this.disposeReader();
402
+ element.innerHTML = "";
403
+ rendition = this.createRendition(element, width, seedHeight);
404
+ this.readerRendition = rendition;
405
+ this.readerTarget = element;
406
+ } else if (typeof rendition.resize === "function" && rendition.manager?.resize) {
407
+ try {
408
+ rendition.resize(width, seedHeight);
409
+ } catch (error) {
410
+ this.disposeReader();
411
+ element.innerHTML = "";
412
+ rendition = this.createRendition(element, width, seedHeight);
413
+ this.readerRendition = rendition;
414
+ this.readerTarget = element;
415
+ }
416
+ }
417
+ if (rendition) {
418
+ const syncVersion = ++this.heightSyncVersion;
419
+ this.applyRenditionTheme(rendition);
420
+ if (renderVersion !== this.renderVersion) return;
421
+ const recentDestNav = Date.now() - this.lastDestinationNavTime < 200 && this.lastDestinationPageIndex === safePageIndex;
422
+ if (recentDestNav) {
423
+ this.debugLog("renderPage:skip-recent-dest-nav", {
424
+ safePageIndex,
425
+ msSince: Date.now() - this.lastDestinationNavTime
426
+ });
427
+ await this.syncSectionHeight(
428
+ rendition,
429
+ element,
430
+ safePageIndex,
431
+ width,
432
+ seedHeight,
433
+ defaultSectionIndex,
434
+ true
435
+ );
436
+ if (renderVersion !== this.renderVersion) return;
437
+ this.scheduleHeightSync(
438
+ syncVersion,
439
+ rendition,
440
+ element,
441
+ safePageIndex,
442
+ width,
443
+ seedHeight,
444
+ defaultSectionIndex,
445
+ true
446
+ );
447
+ if (renderVersion === this.renderVersion) {
448
+ this.currentPage = safePageIndex + 1;
136
449
  }
137
- });
450
+ } else {
451
+ const sectionIndex = await this.displayWithFallback(
452
+ rendition,
453
+ uniqueTargets,
454
+ () => renderVersion !== this.renderVersion
455
+ );
456
+ if (sectionIndex === void 0) return;
457
+ if (renderVersion !== this.renderVersion) return;
458
+ if (consumedPendingHref && pendingHrefDestination && pendingHrefDestination.includes("#")) {
459
+ const anchored = await this.displayDestinationWithRetry(
460
+ rendition,
461
+ pendingHrefDestination
462
+ );
463
+ this.debugLog("renderPage:anchor-second-pass", {
464
+ href: pendingHrefDestination,
465
+ anchored
466
+ });
467
+ if (renderVersion !== this.renderVersion) return;
468
+ }
469
+ await this.syncSectionHeight(
470
+ rendition,
471
+ element,
472
+ safePageIndex,
473
+ width,
474
+ seedHeight,
475
+ sectionIndex ?? defaultSectionIndex
476
+ );
477
+ if (renderVersion !== this.renderVersion) return;
478
+ this.scheduleHeightSync(
479
+ syncVersion,
480
+ rendition,
481
+ element,
482
+ safePageIndex,
483
+ width,
484
+ seedHeight,
485
+ sectionIndex ?? defaultSectionIndex
486
+ );
487
+ if (renderVersion === this.renderVersion) {
488
+ if (consumedPendingHref) this.pendingHrefDestination = null;
489
+ this.currentPage = safePageIndex + 1;
490
+ }
491
+ }
138
492
  }
139
- } else if (typeof rendition.resize === "function") {
140
- rendition.resize(width, height);
141
- }
142
- if (rendition) {
143
- this.applyRenditionTheme(rendition);
144
- const targetRef = spineItem.href || spineItem.idref || spineItem.cfiBase || pageIndex;
145
- await rendition.display(targetRef);
493
+ } finally {
494
+ releaseCurrent?.();
146
495
  }
147
496
  }
148
497
  async renderTextLayer(pageIndex, container, scale) {
@@ -151,20 +500,25 @@ var EPUBEngine = class extends import_core.BaseDocumentEngine {
151
500
  }
152
501
  async getTextContent(pageIndex) {
153
502
  if (!this.book) return [];
154
- const spineItem = this.spineItems[pageIndex];
503
+ if (this.isCoverPage(pageIndex)) return [];
504
+ const spineIndex = this.toSpineIndex(pageIndex);
505
+ if (spineIndex < 0) return [];
506
+ const spineItem = this.spineItems[spineIndex];
155
507
  if (!spineItem) return [];
156
508
  try {
157
509
  const section = this.book.spine.get(spineItem.idref || spineItem.href);
158
510
  const text = typeof section?.text === "function" ? await section.text() : "";
159
511
  if (!text) return [];
160
- return [{
161
- str: text,
162
- dir: "ltr",
163
- width: 0,
164
- height: 0,
165
- transform: [1, 0, 0, 1, 0, 0],
166
- fontName: "default"
167
- }];
512
+ return [
513
+ {
514
+ str: text,
515
+ dir: "ltr",
516
+ width: 0,
517
+ height: 0,
518
+ transform: [1, 0, 0, 1, 0, 0],
519
+ fontName: "default"
520
+ }
521
+ ];
168
522
  } catch {
169
523
  return [];
170
524
  }
@@ -174,36 +528,53 @@ var EPUBEngine = class extends import_core.BaseDocumentEngine {
174
528
  const nav = await this.book.loaded?.navigation;
175
529
  const toc = nav?.toc ?? [];
176
530
  if (!toc.length) return [];
177
- const mapItem = (item) => {
531
+ await this.ensureDestinationSequence();
532
+ const mapItem = async (item) => {
178
533
  const title = item.label || item.title || "";
179
- const pageIndex = this.getSpineIndexByHref(item.href || "");
180
- const children = Array.isArray(item.subitems) ? item.subitems.map(mapItem) : [];
534
+ const href = item.href || "";
535
+ const resolvedIndex = await this.resolvePageIndexFromHref(href);
536
+ const pageIndex = resolvedIndex ?? -1;
537
+ const children = Array.isArray(item.subitems) ? await Promise.all(item.subitems.map(mapItem)) : [];
181
538
  const outlineItem = { title, pageIndex };
539
+ if (href) {
540
+ outlineItem.dest = { kind: "href", value: href };
541
+ }
182
542
  if (children.length > 0) outlineItem.children = children;
183
543
  return outlineItem;
184
544
  };
185
- return toc.map(mapItem);
545
+ return await Promise.all(toc.map(mapItem));
186
546
  }
187
547
  async getPageIndex(dest) {
188
548
  if (!dest) return null;
189
- if (typeof dest === "string") return this.getSpineIndexByHref(dest);
190
- if (dest.kind === "href") return this.getSpineIndexByHref(dest.value);
191
- if (dest.kind === "pageIndex") return dest.value;
192
- if (dest.kind === "pageNumber") return Math.max(0, dest.value - 1);
549
+ if (typeof dest === "string")
550
+ return await this.resolvePageIndexFromHref(dest);
551
+ if (dest.kind === "href")
552
+ return await this.resolvePageIndexFromHref(dest.value);
553
+ if (dest.kind === "pageIndex")
554
+ return Math.max(0, Math.min(this.getPageCount() - 1, dest.value));
555
+ if (dest.kind === "pageNumber")
556
+ return Math.max(0, Math.min(this.getPageCount() - 1, dest.value - 1));
193
557
  return null;
194
558
  }
195
559
  destroy() {
196
- this.renditions.forEach((rendition) => rendition?.destroy?.());
197
- this.renditions.clear();
198
- this.renditionTargets.clear();
560
+ this.renderVersion += 1;
561
+ this.renderLock = Promise.resolve();
562
+ this.pendingHrefDestination = null;
563
+ this.destinationSequence = [];
564
+ this.destinationCursor = -1;
565
+ this.lastDestinationNavTime = 0;
566
+ this.lastDestinationPageIndex = null;
567
+ this.lastDestinationHref = null;
568
+ this.disposeReader();
199
569
  this.pageSizes.clear();
200
570
  if (this.book?.destroy) this.book.destroy();
201
571
  this.book = null;
202
572
  this.spineItems = [];
573
+ this.coverUrl = null;
203
574
  }
204
575
  applyRenditionTheme(rendition) {
205
576
  if (!rendition) return;
206
- const fontSize = `${Math.round(this.zoom * 100)}%`;
577
+ const fontSize = "100%";
207
578
  if (rendition.themes?.fontSize) {
208
579
  rendition.themes.fontSize(fontSize);
209
580
  } else if (rendition.themes?.override) {
@@ -212,13 +583,457 @@ var EPUBEngine = class extends import_core.BaseDocumentEngine {
212
583
  }
213
584
  getSpineIndexByHref(href) {
214
585
  const normalized = this.normalizeHref(href);
586
+ const decoded = this.decodeHref(normalized);
215
587
  if (!normalized) return -1;
216
- const index = this.spineItems.findIndex((item) => this.normalizeHref(item.href) === normalized);
217
- return index;
588
+ const candidates = new Set([normalized, decoded].filter(Boolean));
589
+ const exactIndex = this.spineItems.findIndex((item) => {
590
+ const itemHref = this.normalizeHref(item.href);
591
+ const itemDecoded = this.decodeHref(itemHref);
592
+ return candidates.has(itemHref) || candidates.has(itemDecoded);
593
+ });
594
+ if (exactIndex >= 0) return exactIndex;
595
+ return this.spineItems.findIndex((item) => {
596
+ const itemHref = this.normalizeHref(item.href);
597
+ if (!itemHref) return false;
598
+ const itemDecoded = this.decodeHref(itemHref);
599
+ for (const candidate of candidates) {
600
+ if (itemHref.endsWith(candidate) || candidate.endsWith(itemHref) || itemDecoded.endsWith(candidate) || candidate.endsWith(itemDecoded)) {
601
+ return true;
602
+ }
603
+ }
604
+ return false;
605
+ });
218
606
  }
219
607
  normalizeHref(href) {
608
+ if (!href) return "";
220
609
  return href.split("#")[0];
221
610
  }
611
+ decodeHref(href) {
612
+ if (!href) return "";
613
+ try {
614
+ return decodeURIComponent(href);
615
+ } catch {
616
+ return href;
617
+ }
618
+ }
619
+ getSectionFromHref(href) {
620
+ const normalized = this.normalizeHref(href);
621
+ if (!normalized || !this.book?.spine?.get) return null;
622
+ const decoded = this.decodeHref(normalized);
623
+ const candidates = [normalized, decoded];
624
+ for (const candidate of candidates) {
625
+ if (!candidate) continue;
626
+ try {
627
+ const section = this.book.spine.get(candidate);
628
+ if (section) return section;
629
+ } catch {
630
+ }
631
+ }
632
+ return null;
633
+ }
634
+ getSpineItemForPage(pageIndex) {
635
+ const spineIndex = this.toSpineIndex(pageIndex);
636
+ if (spineIndex < 0 || spineIndex >= this.spineItems.length) return null;
637
+ return this.spineItems[spineIndex] ?? null;
638
+ }
639
+ getSpineIndexForSection(section) {
640
+ if (!section) return -1;
641
+ const sectionIdRef = typeof section?.idref === "string" ? section.idref.trim() : "";
642
+ if (sectionIdRef) {
643
+ const idrefIndex = this.spineItems.findIndex(
644
+ (item) => typeof item?.idref === "string" && item.idref.trim() === sectionIdRef
645
+ );
646
+ if (idrefIndex >= 0) return idrefIndex;
647
+ }
648
+ const sectionHref = typeof section?.href === "string" ? this.normalizeHref(section.href) : "";
649
+ if (sectionHref) {
650
+ const hrefIndex = this.getSpineIndexByHref(sectionHref);
651
+ if (hrefIndex >= 0) return hrefIndex;
652
+ }
653
+ if (typeof section?.index === "number" && section.index >= 0) {
654
+ const directItem = this.spineItems[section.index];
655
+ if (directItem) {
656
+ const directHref = this.normalizeHref(directItem?.href ?? "");
657
+ const directIdRef = typeof directItem?.idref === "string" ? directItem.idref.trim() : "";
658
+ const hasSameHref = Boolean(sectionHref && directHref === sectionHref);
659
+ const hasSameIdRef = Boolean(
660
+ sectionIdRef && directIdRef && directIdRef === sectionIdRef
661
+ );
662
+ if (hasSameHref || hasSameIdRef) return section.index;
663
+ }
664
+ }
665
+ return -1;
666
+ }
667
+ getPageIndexFromSection(section) {
668
+ const spineIndex = this.getSpineIndexForSection(section);
669
+ if (spineIndex < 0) return null;
670
+ return this.toPageIndexFromSpine(spineIndex);
671
+ }
672
+ isHrefMatchingSpineItem(href, spineItem) {
673
+ const normalizedCandidate = this.normalizeHref(href);
674
+ if (!normalizedCandidate) return false;
675
+ const decodedCandidate = this.decodeHref(normalizedCandidate);
676
+ const itemHref = this.normalizeHref(spineItem?.href ?? "");
677
+ if (!itemHref) return false;
678
+ const itemDecoded = this.decodeHref(itemHref);
679
+ for (const candidate of [normalizedCandidate, decodedCandidate]) {
680
+ if (!candidate) continue;
681
+ if (itemHref === candidate || itemDecoded === candidate || itemHref.endsWith(candidate) || candidate.endsWith(itemHref) || itemDecoded.endsWith(candidate) || candidate.endsWith(itemDecoded)) {
682
+ return true;
683
+ }
684
+ }
685
+ return false;
686
+ }
687
+ uniqueDisplayTargets(targets) {
688
+ const deduped = [];
689
+ const seen = /* @__PURE__ */ new Set();
690
+ for (const target of targets) {
691
+ if (typeof target !== "string" && typeof target !== "number") continue;
692
+ const normalized = typeof target === "string" ? target.trim() : String(target).trim();
693
+ if (!normalized) continue;
694
+ const key = `${typeof target}:${normalized}`;
695
+ if (seen.has(key)) continue;
696
+ seen.add(key);
697
+ deduped.push(target);
698
+ }
699
+ return deduped;
700
+ }
701
+ getDisplayTargetsForSpineItem(spineItem) {
702
+ const targets = [];
703
+ const seen = /* @__PURE__ */ new Set();
704
+ const pushTarget = (value) => {
705
+ if (typeof value !== "string" && typeof value !== "number") return;
706
+ const normalized = typeof value === "string" ? value.trim() : String(value).trim();
707
+ if (!normalized) return;
708
+ const key = `${typeof value}:${normalized}`;
709
+ if (seen.has(key)) return;
710
+ seen.add(key);
711
+ targets.push(value);
712
+ };
713
+ pushTarget(spineItem?.href);
714
+ pushTarget(this.decodeHref(spineItem?.href ?? ""));
715
+ pushTarget(spineItem?.idref);
716
+ pushTarget(spineItem?.cfiBase);
717
+ if (typeof spineItem?.index === "number") pushTarget(spineItem.index);
718
+ const sectionFromHref = this.getSectionFromHref(spineItem?.href ?? "");
719
+ if (sectionFromHref) {
720
+ pushTarget(sectionFromHref.href);
721
+ pushTarget(sectionFromHref.idref);
722
+ pushTarget(sectionFromHref.cfiBase);
723
+ if (typeof sectionFromHref.index === "number")
724
+ pushTarget(sectionFromHref.index);
725
+ }
726
+ return targets;
727
+ }
728
+ normalizeDestinationKey(href) {
729
+ if (!href) return "";
730
+ const trimmed = href.trim();
731
+ if (!trimmed) return "";
732
+ return this.decodeHref(trimmed).toLowerCase();
733
+ }
734
+ findDestinationIndexByHref(href) {
735
+ const candidate = this.normalizeDestinationKey(href);
736
+ if (!candidate || !this.destinationSequence.length) return -1;
737
+ const exact = this.destinationSequence.findIndex(
738
+ (entry) => this.normalizeDestinationKey(entry.href) === candidate
739
+ );
740
+ if (exact >= 0) return exact;
741
+ return this.destinationSequence.findIndex((entry) => {
742
+ const key = this.normalizeDestinationKey(entry.href);
743
+ return key === candidate || key.endsWith(candidate) || candidate.endsWith(key);
744
+ });
745
+ }
746
+ findNearestDestinationIndexByPage(pageIndex) {
747
+ if (!this.destinationSequence.length) return -1;
748
+ const candidates = this.destinationSequence.map((entry, idx) => ({ idx, pageIndex: entry.pageIndex })).filter((entry) => entry.pageIndex <= pageIndex);
749
+ if (candidates.length) return candidates[candidates.length - 1].idx;
750
+ const firstNext = this.destinationSequence.findIndex(
751
+ (entry) => entry.pageIndex >= pageIndex
752
+ );
753
+ return firstNext >= 0 ? firstNext : this.destinationSequence.length - 1;
754
+ }
755
+ async ensureDestinationSequence() {
756
+ if (this.destinationSequence.length) return;
757
+ if (!this.book?.loaded?.navigation) return;
758
+ const nav = await this.book.loaded.navigation;
759
+ const toc = nav?.toc ?? [];
760
+ if (!Array.isArray(toc) || !toc.length) return;
761
+ const hrefs = [];
762
+ const walk = (items) => {
763
+ for (const item of items) {
764
+ const href = typeof item?.href === "string" ? item.href.trim() : "";
765
+ if (href) hrefs.push(href);
766
+ if (Array.isArray(item?.subitems) && item.subitems.length) {
767
+ walk(item.subitems);
768
+ }
769
+ }
770
+ };
771
+ walk(toc);
772
+ if (!hrefs.length) return;
773
+ const deduped = [];
774
+ const seen = /* @__PURE__ */ new Set();
775
+ for (const href of hrefs) {
776
+ const key = this.normalizeDestinationKey(href);
777
+ if (!key || seen.has(key)) continue;
778
+ seen.add(key);
779
+ deduped.push(href);
780
+ }
781
+ const sequence = [];
782
+ for (const href of deduped) {
783
+ const pageIndex = await this.resolvePageIndexFromHref(href);
784
+ if (pageIndex == null) continue;
785
+ sequence.push({ href, pageIndex });
786
+ }
787
+ this.destinationSequence = sequence;
788
+ if (this.destinationCursor < 0 && sequence.length) {
789
+ this.destinationCursor = this.findNearestDestinationIndexByPage(
790
+ this.currentPage - 1
791
+ );
792
+ }
793
+ }
794
+ updateDestinationCursor(href, pageIndex) {
795
+ if (!this.destinationSequence.length) {
796
+ void this.ensureDestinationSequence().then(() => {
797
+ if (!this.destinationSequence.length) return;
798
+ this.updateDestinationCursor(href, pageIndex);
799
+ });
800
+ return;
801
+ }
802
+ const byHref = this.findDestinationIndexByHref(href);
803
+ if (byHref >= 0) {
804
+ this.destinationCursor = byHref;
805
+ return;
806
+ }
807
+ if (typeof pageIndex === "number" && pageIndex >= 0) {
808
+ const byPage = this.findNearestDestinationIndexByPage(pageIndex);
809
+ if (byPage >= 0) this.destinationCursor = byPage;
810
+ }
811
+ }
812
+ getSectionFromTarget(target) {
813
+ if (!this.book?.spine?.get) return null;
814
+ try {
815
+ const section = this.book.spine.get(target);
816
+ if (section) return section;
817
+ } catch {
818
+ }
819
+ if (typeof target === "string") {
820
+ return this.getSectionFromHref(target);
821
+ }
822
+ return null;
823
+ }
824
+ getPageIndexFromRenditionLocation(rendition) {
825
+ const location = rendition?.currentLocation?.();
826
+ if (!location) return null;
827
+ const starts = Array.isArray(location) ? location.map((entry) => entry?.start).filter(Boolean) : [location?.start];
828
+ for (const start of starts) {
829
+ if (!start) continue;
830
+ const href = typeof start?.href === "string" ? this.normalizeHref(start.href) : "";
831
+ if (href) {
832
+ const hrefIndex = this.getSpineIndexByHref(href);
833
+ if (hrefIndex >= 0) return this.toPageIndexFromSpine(hrefIndex);
834
+ }
835
+ if (typeof start?.index === "number" && start.index >= 0) {
836
+ const section = this.getSectionFromTarget(start.index);
837
+ const sectionPageIndex = this.getPageIndexFromSection(section);
838
+ if (sectionPageIndex != null) return sectionPageIndex;
839
+ }
840
+ }
841
+ return null;
842
+ }
843
+ isNoSectionError(error) {
844
+ if (!error || typeof error !== "object") return false;
845
+ const message = "message" in error && typeof error.message === "string" ? error.message : "";
846
+ return message.includes("No Section Found");
847
+ }
848
+ isTransientDisplayError(error) {
849
+ if (!error || typeof error !== "object") return false;
850
+ const message = "message" in error && typeof error.message === "string" ? error.message : "";
851
+ if (!message) return false;
852
+ const normalized = message.toLowerCase();
853
+ return normalized.includes("cannot read properties of undefined") || normalized.includes("cannot read properties of null") || normalized.includes("reading 'package'") || normalized.includes('reading "package"');
854
+ }
855
+ async displayWithFallback(rendition, targets, isCancelled) {
856
+ for (const target of targets) {
857
+ if (isCancelled()) return null;
858
+ try {
859
+ await rendition.display(target);
860
+ if (isCancelled()) return null;
861
+ const section = this.getSectionFromTarget(target);
862
+ return typeof section?.index === "number" ? section.index : null;
863
+ } catch (error) {
864
+ if (this.isNoSectionError(error) || this.isTransientDisplayError(error)) {
865
+ continue;
866
+ }
867
+ throw error;
868
+ }
869
+ }
870
+ return void 0;
871
+ }
872
+ async displayDestinationWithRetry(rendition, href) {
873
+ const targets = this.uniqueDisplayTargets([href, this.decodeHref(href)]);
874
+ if (!targets.length) return false;
875
+ const hasAnchor = href.includes("#");
876
+ for (let attempt = 0; attempt < 3; attempt += 1) {
877
+ try {
878
+ const firstPass = await this.displayWithFallback(
879
+ rendition,
880
+ targets,
881
+ () => false
882
+ );
883
+ if (firstPass === void 0) {
884
+ this.debugLog("displayDestinationWithRetry:first-pass-miss", {
885
+ href,
886
+ attempt
887
+ });
888
+ continue;
889
+ }
890
+ if (!hasAnchor) return true;
891
+ await new Promise((resolve) => setTimeout(resolve, 0));
892
+ const secondPass = await this.displayWithFallback(
893
+ rendition,
894
+ targets,
895
+ () => false
896
+ );
897
+ if (secondPass !== void 0) return true;
898
+ this.debugLog("displayDestinationWithRetry:second-pass-miss", {
899
+ href,
900
+ attempt
901
+ });
902
+ } catch {
903
+ this.debugLog("displayDestinationWithRetry:error", { href, attempt });
904
+ }
905
+ await new Promise((resolve) => setTimeout(resolve, 40 * (attempt + 1)));
906
+ }
907
+ return false;
908
+ }
909
+ isDebugEnabled() {
910
+ try {
911
+ const globalFlag = Boolean(globalThis?.__PAPYRUS_EPUB_DEBUG__);
912
+ if (globalFlag) return true;
913
+ const localStorageRef = globalThis?.localStorage;
914
+ if (!localStorageRef) return false;
915
+ const persisted = localStorageRef.getItem("papyrus:epubDebug");
916
+ return persisted === "1" || persisted === "true";
917
+ } catch {
918
+ return false;
919
+ }
920
+ }
921
+ debugLog(message, payload) {
922
+ if (!this.isDebugEnabled()) return;
923
+ if (payload === void 0) {
924
+ console.log("[EPUBEngine]", message);
925
+ return;
926
+ }
927
+ console.log("[EPUBEngine]", message, payload);
928
+ }
929
+ async syncSectionHeight(rendition, element, pageIndex, width, seedHeight, targetSectionIndex, skipResize = false) {
930
+ if (_EPUBEngine.USE_INTERNAL_IFRAME_SCROLL) {
931
+ const fallbackHeight = Math.max(360, Math.ceil(seedHeight));
932
+ element.style.height = `${fallbackHeight}px`;
933
+ this.pageSizes.set(pageIndex, { width, height: fallbackHeight });
934
+ if (!skipResize && typeof rendition?.resize === "function" && rendition.manager?.resize) {
935
+ try {
936
+ rendition.resize(width, fallbackHeight);
937
+ } catch {
938
+ }
939
+ }
940
+ return;
941
+ }
942
+ const measureElementHeight = (elementToMeasure) => {
943
+ const measuredElement = elementToMeasure;
944
+ if (!measuredElement) return 0;
945
+ const rectHeight = typeof measuredElement.getBoundingClientRect === "function" ? measuredElement.getBoundingClientRect().height : 0;
946
+ return Math.max(
947
+ measuredElement.scrollHeight ?? 0,
948
+ measuredElement.offsetHeight ?? 0,
949
+ measuredElement.clientHeight ?? 0,
950
+ Number.isFinite(rectHeight) ? rectHeight : 0
951
+ );
952
+ };
953
+ const measureDocumentHeight = (doc) => {
954
+ if (!doc) return 0;
955
+ return Math.max(
956
+ measureElementHeight(doc.documentElement),
957
+ measureElementHeight(doc.body)
958
+ );
959
+ };
960
+ const measureContentHeight = (strictTarget) => {
961
+ let measured = 0;
962
+ const contents = typeof rendition?.getContents === "function" ? rendition.getContents() : [];
963
+ if (Array.isArray(contents)) {
964
+ for (const content of contents) {
965
+ if (strictTarget && targetSectionIndex !== null && typeof content?.sectionIndex === "number" && content.sectionIndex !== targetSectionIndex) {
966
+ continue;
967
+ }
968
+ const doc = content?.document;
969
+ if (!doc) continue;
970
+ measured = Math.max(measured, measureDocumentHeight(doc));
971
+ }
972
+ }
973
+ if (measured > 0) return measured;
974
+ const frameSelector = strictTarget && targetSectionIndex !== null ? `.epub-view[ref="${targetSectionIndex}"] iframe` : "iframe";
975
+ const frame = element.querySelector(
976
+ frameSelector
977
+ );
978
+ const fallbackFrame = frame ?? (strictTarget && targetSectionIndex !== null ? element.querySelector("iframe") : null);
979
+ const selectedFrame = fallbackFrame ?? frame;
980
+ const frameDoc = selectedFrame?.contentDocument;
981
+ if (!frameDoc) return 0;
982
+ return measureDocumentHeight(frameDoc);
983
+ };
984
+ let contentHeight = measureContentHeight(true);
985
+ if (contentHeight <= 0 && targetSectionIndex !== null) {
986
+ contentHeight = measureContentHeight(false);
987
+ }
988
+ if (contentHeight <= 0) {
989
+ await new Promise((resolve) => setTimeout(resolve, 0));
990
+ contentHeight = measureContentHeight(true);
991
+ if (contentHeight <= 0 && targetSectionIndex !== null) {
992
+ contentHeight = measureContentHeight(false);
993
+ }
994
+ }
995
+ const measuredTarget = contentHeight > 0 ? Math.ceil(contentHeight + _EPUBEngine.HEIGHT_PADDING) : Math.ceil(seedHeight);
996
+ const minA4Height = this.getA4MinHeight(width);
997
+ const unclampedTarget = Math.max(minA4Height, measuredTarget);
998
+ const targetHeight = Math.max(
999
+ minA4Height,
1000
+ Math.min(_EPUBEngine.MAX_SECTION_HEIGHT, unclampedTarget)
1001
+ );
1002
+ if (targetHeight !== unclampedTarget) {
1003
+ this.debugLog("syncSectionHeight:clamped", {
1004
+ pageIndex,
1005
+ measuredTarget,
1006
+ max: _EPUBEngine.MAX_SECTION_HEIGHT
1007
+ });
1008
+ }
1009
+ if (targetHeight <= 0) return;
1010
+ element.style.height = `${targetHeight}px`;
1011
+ this.pageSizes.set(pageIndex, { width, height: targetHeight });
1012
+ if (!skipResize && typeof rendition?.resize === "function" && rendition.manager?.resize) {
1013
+ try {
1014
+ rendition.resize(width, targetHeight);
1015
+ } catch {
1016
+ }
1017
+ }
1018
+ }
1019
+ async resolvePageIndexFromHref(href) {
1020
+ const normalizedHref = href.trim();
1021
+ if (!normalizedHref) return null;
1022
+ const section = this.getSectionFromHref(normalizedHref);
1023
+ const sectionPageIndex = this.getPageIndexFromSection(section);
1024
+ if (sectionPageIndex != null) return sectionPageIndex;
1025
+ const spineIndex = this.getSpineIndexByHref(
1026
+ this.normalizeHref(normalizedHref)
1027
+ );
1028
+ return spineIndex >= 0 ? this.toPageIndexFromSpine(spineIndex) : null;
1029
+ }
1030
+ extractHrefDestination(dest) {
1031
+ if (typeof dest === "string") return dest;
1032
+ if (dest?.kind === "href" && typeof dest.value === "string") {
1033
+ return dest.value;
1034
+ }
1035
+ return null;
1036
+ }
222
1037
  isUriSource(source) {
223
1038
  return typeof source === "object" && source !== null && "uri" in source;
224
1039
  }
@@ -266,7 +1081,9 @@ var EPUBEngine = class extends import_core.BaseDocumentEngine {
266
1081
  decodeBase64(value) {
267
1082
  const clean = value.replace(/\s/g, "");
268
1083
  if (typeof atob !== "function") {
269
- throw new Error("[EPUBEngine] atob n\xE3o est\xE1 dispon\xEDvel para decodificar base64.");
1084
+ throw new Error(
1085
+ "[EPUBEngine] atob n\xE3o est\xE1 dispon\xEDvel para decodificar base64."
1086
+ );
270
1087
  }
271
1088
  const binary = atob(clean);
272
1089
  const len = binary.length;
@@ -285,6 +1102,372 @@ var EPUBEngine = class extends import_core.BaseDocumentEngine {
285
1102
  if (value.length < 16) return false;
286
1103
  return /^[A-Za-z0-9+/=]+$/.test(value);
287
1104
  }
1105
+ createRendition(element, width, height) {
1106
+ const rendition = this.book.renderTo(element, {
1107
+ width,
1108
+ height,
1109
+ flow: "scrolled-doc",
1110
+ spread: "none",
1111
+ method: "write",
1112
+ overflow: _EPUBEngine.USE_INTERNAL_IFRAME_SCROLL ? "auto" : "hidden"
1113
+ });
1114
+ this.registerContentHook(rendition);
1115
+ return rendition;
1116
+ }
1117
+ disposeReader() {
1118
+ const rendition = this.readerRendition;
1119
+ if (!rendition) return;
1120
+ try {
1121
+ rendition?.manager?.destroy?.();
1122
+ } catch {
1123
+ }
1124
+ this.readerRendition = null;
1125
+ this.readerTarget = null;
1126
+ this.heightSyncVersion += 1;
1127
+ }
1128
+ registerContentHook(rendition) {
1129
+ if (!rendition?.hooks?.content?.register) return;
1130
+ rendition.hooks.content.register((contents) => {
1131
+ try {
1132
+ const setImportantStyle = (node, property, value) => {
1133
+ if (!node) return;
1134
+ node.style.setProperty(property, value, "important");
1135
+ };
1136
+ const useInternalScroll = _EPUBEngine.USE_INTERNAL_IFRAME_SCROLL;
1137
+ const managerContainer = rendition?.manager?.container;
1138
+ const viewsContainer = rendition?.manager?.views?.container;
1139
+ if (managerContainer) {
1140
+ managerContainer.classList.add("papyrus-epub-scroll-host");
1141
+ managerContainer.style.overflow = useInternalScroll ? "auto" : "hidden";
1142
+ managerContainer.style.overflowY = useInternalScroll ? "auto" : "hidden";
1143
+ managerContainer.style.overflowX = "hidden";
1144
+ managerContainer.style.height = "100%";
1145
+ managerContainer.style.maxHeight = "100%";
1146
+ managerContainer.style.scrollbarWidth = useInternalScroll ? "thin" : "none";
1147
+ managerContainer.style.setProperty(
1148
+ "-webkit-overflow-scrolling",
1149
+ "touch"
1150
+ );
1151
+ managerContainer.style.setProperty("overscroll-behavior", "contain");
1152
+ }
1153
+ if (viewsContainer) {
1154
+ viewsContainer.style.overflow = useInternalScroll ? "visible" : "hidden";
1155
+ viewsContainer.style.overflowY = useInternalScroll ? "visible" : "hidden";
1156
+ viewsContainer.style.overflowX = "hidden";
1157
+ viewsContainer.style.minHeight = "100%";
1158
+ viewsContainer.style.scrollbarWidth = useInternalScroll ? "auto" : "none";
1159
+ }
1160
+ const frame = contents?.window?.frameElement;
1161
+ const mobileProbe = managerContainer ?? frame ?? contents?.document?.body;
1162
+ const mobileWidthHint = Math.max(
1163
+ managerContainer?.clientWidth ?? 0,
1164
+ frame?.clientWidth ?? 0,
1165
+ mobileProbe?.clientWidth ?? 0
1166
+ );
1167
+ const isMobileInternalScroll = Boolean(
1168
+ useInternalScroll && mobileProbe && this.isMobileViewport(mobileProbe, mobileWidthHint)
1169
+ );
1170
+ const contentOverflow = useInternalScroll ? "hidden" : "visible";
1171
+ const doc = contents?.document;
1172
+ const root = doc?.documentElement;
1173
+ const body = doc?.body;
1174
+ contents?.overflow?.(contentOverflow);
1175
+ contents?.overflowX?.("hidden");
1176
+ contents?.overflowY?.(contentOverflow);
1177
+ contents?.css?.("overflow", contentOverflow, true);
1178
+ contents?.css?.("overflow-y", contentOverflow, true);
1179
+ contents?.css?.("overflow-x", "hidden", true);
1180
+ contents?.css?.("height", "auto", true);
1181
+ contents?.css?.("max-height", "none", true);
1182
+ contents?.css?.("min-height", "100%", true);
1183
+ if (root) {
1184
+ setImportantStyle(root, "overflow", contentOverflow);
1185
+ setImportantStyle(root, "overflow-y", contentOverflow);
1186
+ setImportantStyle(root, "overflow-x", "hidden");
1187
+ setImportantStyle(root, "height", "auto");
1188
+ setImportantStyle(root, "min-height", "100%");
1189
+ setImportantStyle(root, "max-height", "none");
1190
+ setImportantStyle(root, "scrollbar-width", "none");
1191
+ setImportantStyle(root, "overscroll-behavior", "none");
1192
+ }
1193
+ if (body) {
1194
+ setImportantStyle(body, "overflow", contentOverflow);
1195
+ setImportantStyle(body, "overflow-y", contentOverflow);
1196
+ setImportantStyle(body, "overflow-x", "hidden");
1197
+ setImportantStyle(body, "height", "auto");
1198
+ setImportantStyle(body, "min-height", "100%");
1199
+ setImportantStyle(body, "max-height", "none");
1200
+ setImportantStyle(body, "scrollbar-width", "none");
1201
+ setImportantStyle(body, "overscroll-behavior", "none");
1202
+ if (isMobileInternalScroll) {
1203
+ setImportantStyle(body, "margin", "0");
1204
+ setImportantStyle(body, "padding", "0");
1205
+ }
1206
+ }
1207
+ if (useInternalScroll && managerContainer && doc && root) {
1208
+ const proxyMarker = "data-papyrus-scroll-proxy";
1209
+ if (!root.hasAttribute(proxyMarker)) {
1210
+ root.setAttribute(proxyMarker, "1");
1211
+ const scrollHost = managerContainer;
1212
+ const onWheel = (event) => {
1213
+ if (event.defaultPrevented) return;
1214
+ if (event.ctrlKey || event.metaKey) return;
1215
+ const deltaY = Number(event.deltaY) || 0;
1216
+ const deltaX = Number(event.deltaX) || 0;
1217
+ if (!deltaY && !deltaX) return;
1218
+ const previousTop = scrollHost.scrollTop;
1219
+ const previousLeft = scrollHost.scrollLeft;
1220
+ scrollHost.scrollTop += deltaY;
1221
+ scrollHost.scrollLeft += deltaX;
1222
+ const moved = scrollHost.scrollTop !== previousTop || scrollHost.scrollLeft !== previousLeft;
1223
+ if (moved && event.cancelable) event.preventDefault();
1224
+ };
1225
+ let lastTouchY = null;
1226
+ let lastTouchX = null;
1227
+ const onTouchStart = (event) => {
1228
+ if (event.touches.length !== 1) return;
1229
+ lastTouchY = event.touches[0].clientY;
1230
+ lastTouchX = event.touches[0].clientX;
1231
+ };
1232
+ const onTouchMove = (event) => {
1233
+ if (event.touches.length !== 1) return;
1234
+ const touch = event.touches[0];
1235
+ if (lastTouchY == null || lastTouchX == null) {
1236
+ lastTouchY = touch.clientY;
1237
+ lastTouchX = touch.clientX;
1238
+ return;
1239
+ }
1240
+ const deltaY = lastTouchY - touch.clientY;
1241
+ const deltaX = lastTouchX - touch.clientX;
1242
+ const previousTop = scrollHost.scrollTop;
1243
+ const previousLeft = scrollHost.scrollLeft;
1244
+ if (deltaY || deltaX) {
1245
+ scrollHost.scrollTop += deltaY;
1246
+ scrollHost.scrollLeft += deltaX;
1247
+ }
1248
+ const moved = scrollHost.scrollTop !== previousTop || scrollHost.scrollLeft !== previousLeft;
1249
+ if (moved && event.cancelable) event.preventDefault();
1250
+ lastTouchY = touch.clientY;
1251
+ lastTouchX = touch.clientX;
1252
+ };
1253
+ const onTouchEnd = () => {
1254
+ lastTouchY = null;
1255
+ lastTouchX = null;
1256
+ };
1257
+ doc.addEventListener("wheel", onWheel, { passive: false });
1258
+ doc.addEventListener("touchstart", onTouchStart, {
1259
+ passive: true
1260
+ });
1261
+ doc.addEventListener("touchmove", onTouchMove, {
1262
+ passive: false
1263
+ });
1264
+ doc.addEventListener("touchend", onTouchEnd, { passive: true });
1265
+ doc.addEventListener("touchcancel", onTouchEnd, {
1266
+ passive: true
1267
+ });
1268
+ }
1269
+ }
1270
+ if (isMobileInternalScroll && doc?.body) {
1271
+ const singleImage = this.getSingleImageForMobileLayout(doc);
1272
+ if (singleImage) {
1273
+ doc.body.style.display = "flex";
1274
+ doc.body.style.justifyContent = "center";
1275
+ doc.body.style.alignItems = "flex-start";
1276
+ singleImage.style.width = "100%";
1277
+ singleImage.style.maxWidth = "100%";
1278
+ singleImage.style.height = "auto";
1279
+ singleImage.style.maxHeight = "100%";
1280
+ singleImage.style.display = "block";
1281
+ singleImage.style.objectFit = "contain";
1282
+ }
1283
+ }
1284
+ if (frame) {
1285
+ if (useInternalScroll) {
1286
+ frame.setAttribute("scrolling", "no");
1287
+ setImportantStyle(frame, "overflow", "hidden");
1288
+ setImportantStyle(frame, "overflow-y", "hidden");
1289
+ setImportantStyle(frame, "overflow-x", "hidden");
1290
+ setImportantStyle(frame, "height", "auto");
1291
+ setImportantStyle(frame, "min-height", "100%");
1292
+ setImportantStyle(frame, "max-height", "none");
1293
+ setImportantStyle(frame, "width", "100%");
1294
+ setImportantStyle(frame, "max-width", "100%");
1295
+ setImportantStyle(frame, "display", "block");
1296
+ setImportantStyle(frame, "border", "0");
1297
+ } else {
1298
+ frame.setAttribute("scrolling", "no");
1299
+ setImportantStyle(frame, "overflow", "hidden");
1300
+ setImportantStyle(frame, "height", "auto");
1301
+ }
1302
+ }
1303
+ } catch {
1304
+ }
1305
+ });
1306
+ }
1307
+ scheduleHeightSync(syncVersion, rendition, element, pageIndex, width, fallbackHeight, targetSectionIndex, skipResize = false) {
1308
+ const run = () => {
1309
+ if (syncVersion !== this.heightSyncVersion) return;
1310
+ if (this.readerRendition !== rendition) return;
1311
+ if (this.readerTarget !== element) return;
1312
+ void this.syncSectionHeight(
1313
+ rendition,
1314
+ element,
1315
+ pageIndex,
1316
+ width,
1317
+ fallbackHeight,
1318
+ targetSectionIndex,
1319
+ skipResize
1320
+ );
1321
+ };
1322
+ setTimeout(run, 80);
1323
+ setTimeout(run, 260);
1324
+ setTimeout(run, 620);
1325
+ setTimeout(run, 1200);
1326
+ }
1327
+ getViewportHeightHint(element) {
1328
+ const viewerRoot = element.closest(".papyrus-viewer");
1329
+ const hostParent = element.parentElement;
1330
+ const measured = Math.max(
1331
+ viewerRoot?.clientHeight ?? 0,
1332
+ hostParent?.clientHeight ?? 0,
1333
+ element.clientHeight ?? 0
1334
+ );
1335
+ if (measured <= 0) return null;
1336
+ const viewportWidth = Math.max(
1337
+ viewerRoot?.clientWidth ?? 0,
1338
+ globalThis?.visualViewport?.width ?? 0,
1339
+ globalThis?.innerWidth ?? 0
1340
+ );
1341
+ const isMobileViewport = viewportWidth > 0 && viewportWidth <= _EPUBEngine.MOBILE_VIEWPORT_MAX_WIDTH_PX;
1342
+ if (!isMobileViewport) return null;
1343
+ let topbarOffset = 0;
1344
+ const shell = viewerRoot?.parentElement?.parentElement;
1345
+ if (shell) {
1346
+ const shellHeight = shell.getBoundingClientRect().height;
1347
+ const topbar = Array.from(shell.children).find(
1348
+ (child) => child.classList?.contains("papyrus-topbar")
1349
+ );
1350
+ const topbarHeight = topbar?.getBoundingClientRect().height ?? 0;
1351
+ const viewerLikelyIncludesTopbar = topbarHeight > 0 && shellHeight > 0 && measured >= shellHeight - 2;
1352
+ if (viewerLikelyIncludesTopbar) {
1353
+ topbarOffset = Math.ceil(topbarHeight);
1354
+ }
1355
+ }
1356
+ const adjusted = measured - topbarOffset - _EPUBEngine.INTERNAL_VIEWPORT_PADDING_PX;
1357
+ return Math.max(280, Math.floor(adjusted));
1358
+ }
1359
+ getLinearSpineItems(items) {
1360
+ const linearItems = items.filter((item) => item?.linear !== "no");
1361
+ if (!linearItems.length) return items;
1362
+ const deduped = [];
1363
+ for (const item of linearItems) {
1364
+ const prev = deduped[deduped.length - 1];
1365
+ const prevHref = this.normalizeHref(prev?.href ?? "");
1366
+ const currentHref = this.normalizeHref(item?.href ?? "");
1367
+ if (prevHref && currentHref && prevHref === currentHref) continue;
1368
+ deduped.push(item);
1369
+ }
1370
+ return deduped.length ? deduped : linearItems;
1371
+ }
1372
+ hasCoverPage() {
1373
+ return Boolean(this.coverUrl);
1374
+ }
1375
+ isCoverPage(pageIndex) {
1376
+ return this.hasCoverPage() && pageIndex === 0;
1377
+ }
1378
+ toSpineIndex(pageIndex) {
1379
+ return pageIndex - (this.hasCoverPage() ? 1 : 0);
1380
+ }
1381
+ toPageIndexFromSpine(spineIndex) {
1382
+ return spineIndex + (this.hasCoverPage() ? 1 : 0);
1383
+ }
1384
+ getA4MinHeight(width) {
1385
+ if (!Number.isFinite(width) || width <= 0) return 900;
1386
+ return Math.max(480, Math.ceil(width * _EPUBEngine.A4_RATIO));
1387
+ }
1388
+ renderCoverPage(element, width, height, pageIndex) {
1389
+ this.disposeReader();
1390
+ element.innerHTML = "";
1391
+ element.style.width = `${width}px`;
1392
+ element.style.height = `${height}px`;
1393
+ const isMobileViewport = this.isMobileViewport(element, width);
1394
+ const isLandscapeViewport = width > height;
1395
+ const wrapper = document.createElement("div");
1396
+ wrapper.style.width = "100%";
1397
+ wrapper.style.minHeight = `${height}px`;
1398
+ wrapper.style.display = "flex";
1399
+ wrapper.style.alignItems = "center";
1400
+ wrapper.style.justifyContent = "center";
1401
+ wrapper.style.background = "#fff";
1402
+ wrapper.style.padding = isMobileViewport ? "0" : "16px";
1403
+ wrapper.style.boxSizing = "border-box";
1404
+ if (isMobileViewport) {
1405
+ wrapper.style.overflow = "hidden";
1406
+ }
1407
+ const img = document.createElement("img");
1408
+ img.src = this.coverUrl ?? "";
1409
+ img.alt = "Capa";
1410
+ img.style.maxWidth = "100%";
1411
+ img.style.maxHeight = "100%";
1412
+ img.style.width = isMobileViewport ? "100%" : "auto";
1413
+ img.style.height = isMobileViewport ? "100%" : "auto";
1414
+ img.style.display = "block";
1415
+ img.style.objectFit = isMobileViewport && !isLandscapeViewport ? "cover" : "contain";
1416
+ img.style.boxShadow = isMobileViewport ? "none" : "0 16px 32px rgba(0,0,0,0.15)";
1417
+ img.style.borderRadius = isMobileViewport ? "0" : "8px";
1418
+ img.onload = () => {
1419
+ if (isMobileViewport) {
1420
+ const targetHeight2 = Math.max(280, Math.floor(height));
1421
+ wrapper.style.minHeight = `${targetHeight2}px`;
1422
+ element.style.height = `${targetHeight2}px`;
1423
+ this.pageSizes.set(pageIndex, { width, height: targetHeight2 });
1424
+ return;
1425
+ }
1426
+ const naturalWidth = img.naturalWidth || width;
1427
+ const naturalHeight = img.naturalHeight || height;
1428
+ if (naturalWidth <= 0 || naturalHeight <= 0) return;
1429
+ const displayedWidth = Math.min(naturalWidth, Math.max(1, width - 32));
1430
+ const scaledHeight = Math.round(
1431
+ naturalHeight / naturalWidth * displayedWidth
1432
+ );
1433
+ const targetHeight = Math.max(height, scaledHeight + 32);
1434
+ wrapper.style.minHeight = `${targetHeight}px`;
1435
+ element.style.height = `${targetHeight}px`;
1436
+ this.pageSizes.set(pageIndex, { width, height: targetHeight });
1437
+ };
1438
+ wrapper.appendChild(img);
1439
+ element.appendChild(wrapper);
1440
+ this.pageSizes.set(pageIndex, { width, height });
1441
+ }
1442
+ isMobileViewport(element, widthHint) {
1443
+ const viewerRoot = element.closest(".papyrus-viewer");
1444
+ const viewportWidth = Math.max(
1445
+ widthHint,
1446
+ viewerRoot?.clientWidth ?? 0,
1447
+ globalThis?.visualViewport?.width ?? 0,
1448
+ globalThis?.innerWidth ?? 0
1449
+ );
1450
+ const viewportHeight = Math.max(
1451
+ viewerRoot?.clientHeight ?? 0,
1452
+ globalThis?.visualViewport?.height ?? 0,
1453
+ globalThis?.innerHeight ?? 0
1454
+ );
1455
+ const isShortLandscape = viewportHeight > 0 && viewportHeight <= _EPUBEngine.MOBILE_SHORT_VIEWPORT_MAX_HEIGHT_PX && viewportWidth > viewportHeight;
1456
+ return Boolean(
1457
+ viewportWidth > 0 && (viewportWidth <= _EPUBEngine.MOBILE_VIEWPORT_MAX_WIDTH_PX || isShortLandscape)
1458
+ );
1459
+ }
1460
+ getSingleImageForMobileLayout(doc) {
1461
+ const body = doc.body;
1462
+ if (!body) return null;
1463
+ const images = Array.from(
1464
+ body.querySelectorAll("img")
1465
+ );
1466
+ if (images.length !== 1) return null;
1467
+ const textLength = (body.textContent ?? "").replace(/\s+/g, "").length;
1468
+ if (textLength > 48) return null;
1469
+ return images[0] ?? null;
1470
+ }
288
1471
  };
289
1472
  // Annotate the CommonJS export names for ESM import in node:
290
1473
  0 && (module.exports = {