@prose-reader/core 1.15.0 → 1.16.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/prose.js CHANGED
@@ -2125,7 +2125,15 @@ const fontsEnhancer = (next) => (options) => {
2125
2125
  });
2126
2126
  const reader = next(options);
2127
2127
  const getStyle = () => `
2128
- ${``}
2128
+ ${/*
2129
+ Ideally, we would like to use !important but it could break publisher specific
2130
+ cases.
2131
+ Also right now we do not apply it to * since it would also break publisher
2132
+ more specific scaling down the tree.
2133
+
2134
+ body *:not([class^="mjx-"]) {
2135
+ */
2136
+ ``}
2129
2137
  body {
2130
2138
  ${settingsSubject$.value.fontScale !== 1 ? `font-size: ${settingsSubject$.value.fontScale}em !important;` : ``}
2131
2139
  ${settingsSubject$.value.lineHeight !== `publisher` ? `line-height: ${settingsSubject$.value.lineHeight} !important;` : ``}
@@ -2167,6 +2175,9 @@ const fontsEnhancer = (next) => (options) => {
2167
2175
  );
2168
2176
  return {
2169
2177
  ...reader,
2178
+ /**
2179
+ * Absorb current enhancer settings and passthrough the rest to reader
2180
+ */
2170
2181
  setSettings: (settings) => {
2171
2182
  const {
2172
2183
  fontJustification: fontJustification2 = settingsSubject$.value.fontJustification,
@@ -2180,6 +2191,9 @@ const fontsEnhancer = (next) => (options) => {
2180
2191
  }
2181
2192
  reader.setSettings(passthroughSettings);
2182
2193
  },
2194
+ /**
2195
+ * Combine reader settings with enhancer settings
2196
+ */
2183
2197
  settings$
2184
2198
  };
2185
2199
  };
@@ -2447,6 +2461,7 @@ const time = (name, targetDuration = 0) => {
2447
2461
  };
2448
2462
  };
2449
2463
  const createReport = (namespace) => ({
2464
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2450
2465
  log: (...data) => {
2451
2466
  if (window.__PROSE_READER_DEBUG) {
2452
2467
  if (namespace)
@@ -2455,6 +2470,7 @@ const createReport = (namespace) => ({
2455
2470
  console.log(wrap(ROOT_NAMESPACE), ...data);
2456
2471
  }
2457
2472
  },
2473
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2458
2474
  warn: (...data) => {
2459
2475
  if (window.__PROSE_READER_DEBUG) {
2460
2476
  if (namespace)
@@ -2463,9 +2479,22 @@ const createReport = (namespace) => ({
2463
2479
  console.warn(wrap(ROOT_NAMESPACE), ...data);
2464
2480
  }
2465
2481
  },
2482
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2466
2483
  error: (...data) => {
2467
2484
  console.error(...data);
2468
2485
  },
2486
+ // time: (label?: string | undefined) => {
2487
+ // if (window.__PROSE_READER_DEBUG) {
2488
+ // // eslint-disable-next-line no-console
2489
+ // console.time(`[prose-reader] [metric] ${label}`);
2490
+ // }
2491
+ // },
2492
+ // timeEnd: (label?: string | undefined) => {
2493
+ // if (window.__PROSE_READER_DEBUG) {
2494
+ // // eslint-disable-next-line no-console
2495
+ // console.timeEnd(`[prose-reader] [metric] ${label}`);
2496
+ // }
2497
+ // },
2469
2498
  time,
2470
2499
  logMetric: (performanceEntry, targetDuration = 0) => {
2471
2500
  if (window.__PROSE_READER_DEBUG) {
@@ -2479,6 +2508,7 @@ const createReport = (namespace) => ({
2479
2508
  }
2480
2509
  }
2481
2510
  },
2511
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2482
2512
  measurePerformance: (name, targetDuration = 10, functionToMeasure, { disable } = {}) => {
2483
2513
  if (disable || !window.__PROSE_READER_DEBUG)
2484
2514
  return functionToMeasure;
@@ -2673,15 +2703,37 @@ const paginationEnhancer = (next) => (options) => {
2673
2703
  beginPageIndexInChapter: paginationInfo.beginPageIndex,
2674
2704
  beginNumberOfPagesInChapter: paginationInfo.beginNumberOfPages,
2675
2705
  beginChapterInfo: beginItem ? chaptersInfo[beginItem.item.id] : void 0,
2706
+ // chapterIndex: number;
2707
+ // pages: number;
2708
+ // pageIndexInBook: number;
2709
+ // pageIndexInChapter: number;
2710
+ // pagesOfChapter: number;
2711
+ // pagePercentageInChapter: number;
2712
+ // offsetPercentageInChapter: number;
2713
+ // domIndex: number;
2714
+ // charOffset: number;
2715
+ // serializeString?: string;
2676
2716
  beginSpineItemIndex: paginationInfo.beginSpineItemIndex,
2717
+ // spineItemPath: beginItem?.item.path,
2718
+ // spineItemId: beginItem?.item.id,
2677
2719
  beginCfi: paginationInfo.beginCfi,
2678
2720
  beginSpineItemReadingDirection: beginItem == null ? void 0 : beginItem.getReadingDirection(),
2679
2721
  endChapterInfo: endItem ? chaptersInfo[endItem.item.id] : void 0,
2680
2722
  endPageIndexInChapter: paginationInfo.endPageIndex,
2681
2723
  endNumberOfPagesInChapter: paginationInfo.endNumberOfPages,
2682
2724
  endSpineItemIndex: paginationInfo.endSpineItemIndex,
2725
+ // spineItemPath: endItem?.item.path,
2726
+ // spineItemId: endItem?.item.id,
2683
2727
  endSpineItemReadingDirection: endItem == null ? void 0 : endItem.getReadingDirection(),
2684
2728
  endCfi: paginationInfo.endCfi,
2729
+ // end: ReadingLocation;
2730
+ // spineItemReadingDirection: focusedSpineItem?.getReadingDirection(),
2731
+ /**
2732
+ * This percentage is based of the weight (kb) of every items and the number of pages.
2733
+ * It is not accurate but gives a general good idea of the overall progress.
2734
+ * It is recommended to use this progress only for reflow books. For pre-paginated books
2735
+ * the number of pages and current index can be used instead since 1 page = 1 chapter.
2736
+ */
2685
2737
  percentageEstimateOfBook: endItem ? reader.progression.getPercentageEstimate(
2686
2738
  context,
2687
2739
  paginationInfo.endSpineItemIndex ?? 0,
@@ -2691,6 +2743,11 @@ const paginationEnhancer = (next) => (options) => {
2691
2743
  endItem
2692
2744
  ) : 0,
2693
2745
  isUsingSpread: context.shouldDisplaySpread()
2746
+ // chaptersOfBook: number;
2747
+ // chapter: string;
2748
+ // hasNextChapter: (reader.spine.spineItemIndex || 0) < (manifest.readingOrder.length - 1),
2749
+ // hasPreviousChapter: (reader.spine.spineItemIndex || 0) < (manifest.readingOrder.length - 1),
2750
+ // numberOfSpineItems: context.getManifest()?.readingOrder.length,
2694
2751
  };
2695
2752
  };
2696
2753
  const getSpineItemNumberOfPages = (spineItem) => {
@@ -2730,6 +2787,9 @@ const paginationEnhancer = (next) => (options) => {
2730
2787
  const numberOfPagesPerItems = getNumberOfPagesPerItems();
2731
2788
  return {
2732
2789
  numberOfPagesPerItems,
2790
+ /**
2791
+ * This may be not accurate for reflowable due to dynamic load / unload.
2792
+ */
2733
2793
  numberOfTotalPages: numberOfPagesPerItems.reduce((acc, numberOfPagesForItem) => acc + numberOfPagesForItem, 0)
2734
2794
  };
2735
2795
  }),
@@ -2820,7 +2880,11 @@ const themeEnhancer = (next) => (options) => {
2820
2880
  }
2821
2881
  ${(foundTheme == null ? void 0 : foundTheme.foregroundColor) ? `
2822
2882
  body * {
2823
- ${``}
2883
+ ${/*
2884
+ Ideally, we would like to use !important but it could break publisher specific
2885
+ cases
2886
+ */
2887
+ ``}
2824
2888
  color: ${foundTheme.foregroundColor};
2825
2889
  }
2826
2890
  ` : ``}
@@ -3418,6 +3482,8 @@ const createLoader = ({
3418
3482
  take(1)
3419
3483
  );
3420
3484
  const unload$ = unloadSubject$.asObservable().pipe(
3485
+ // @todo remove iframe when viewport is free
3486
+ // @todo use takeUntil(load$) when it's the case to cancel
3421
3487
  withLatestFrom(frameElementSubject$),
3422
3488
  filter(([_, frame]) => !!frame),
3423
3489
  map(([, frame]) => {
@@ -3438,13 +3504,16 @@ const createLoader = ({
3438
3504
  const load$ = loadSubject$.asObservable().pipe(
3439
3505
  withLatestFrom(isLoadedSubject$),
3440
3506
  filter(([_, isLoaded]) => !isLoaded),
3507
+ // let's ignore later load as long as the first one still runs
3441
3508
  exhaustMap(() => {
3442
3509
  return createFrame$().pipe(
3443
3510
  mergeMap((frame) => waitForViewportFree$.pipe(map(() => frame))),
3444
3511
  mergeMap((frame) => {
3445
3512
  parent.appendChild(frame);
3446
3513
  frameElementSubject$.next(frame);
3447
- if (!fetchResource && item.href.startsWith(window.location.origin) && (item.mediaType && [`application/xhtml+xml`, `application/xml`, `text/html`, `text/xml`].includes(item.mediaType) || !item.mediaType && ITEM_EXTENSION_VALID_FOR_FRAME_SRC.some((extension) => item.href.endsWith(extension)))) {
3514
+ if (!fetchResource && item.href.startsWith(window.location.origin) && // we have an encoding and it's a valid html
3515
+ (item.mediaType && [`application/xhtml+xml`, `application/xml`, `text/html`, `text/xml`].includes(item.mediaType) || // no encoding ? then try to detect html
3516
+ !item.mediaType && ITEM_EXTENSION_VALID_FOR_FRAME_SRC.some((extension) => item.href.endsWith(extension)))) {
3448
3517
  frame == null ? void 0 : frame.setAttribute(`src`, item.href);
3449
3518
  return of(frame);
3450
3519
  } else {
@@ -3500,6 +3569,7 @@ const createLoader = ({
3500
3569
  })
3501
3570
  );
3502
3571
  }),
3572
+ // we stop loading as soon as unload is requested
3503
3573
  takeUntil(unloadSubject$)
3504
3574
  );
3505
3575
  }),
@@ -3626,6 +3696,12 @@ const createFrameItem = ({
3626
3696
  getHtmlFromResource,
3627
3697
  load,
3628
3698
  unload,
3699
+ /**
3700
+ * Upward layout is used when the parent wants to manipulate the iframe without triggering
3701
+ * `layout` event. This is a particular case needed for iframe because the parent can layout following
3702
+ * an iframe `layout` event. Because the parent `layout` may change some of iframe properties we do not
3703
+ * want the iframe to trigger a new `layout` even and have infinite loop.
3704
+ */
3629
3705
  staticLayout: (size) => {
3630
3706
  const frame = frameElement$.getValue();
3631
3707
  if (frame) {
@@ -3636,6 +3712,8 @@ const createFrameItem = ({
3636
3712
  }
3637
3713
  }
3638
3714
  },
3715
+ // @todo block access, only public API to manipulate / get information (in order to memo / optimize)
3716
+ // manipulate() with cb and return boolean whether re-layout or not
3639
3717
  getManipulableFrame,
3640
3718
  getReadingDirection: () => {
3641
3719
  var _a;
@@ -3657,6 +3735,10 @@ const createFrameItem = ({
3657
3735
  loaded$,
3658
3736
  ready$,
3659
3737
  isReady$: isReadySubject$.asObservable(),
3738
+ /**
3739
+ * This is used as upstream layout change. This event is being listened to by upper app
3740
+ * in order to layout again and adjust every element based on the new content.
3741
+ */
3660
3742
  contentLayoutChange$
3661
3743
  }
3662
3744
  };
@@ -3935,6 +4017,8 @@ const createCommonSpineItem = ({
3935
4017
  const rect = containerElement.getBoundingClientRect();
3936
4018
  const normalizedValues = {
3937
4019
  ...rect,
4020
+ // we want to round to first decimal because it's possible to have half pixel
4021
+ // however browser engine can also gives back x.yyyy based on their precision
3938
4022
  width: Math.round(rect.width * 10) / 10,
3939
4023
  height: Math.round(rect.height * 10) / 10
3940
4024
  };
@@ -4014,6 +4098,8 @@ const createCommonSpineItem = ({
4014
4098
  return {
4015
4099
  columnHeight,
4016
4100
  columnWidth,
4101
+ // horizontalMargin,
4102
+ // verticalMargin,
4017
4103
  width
4018
4104
  };
4019
4105
  };
@@ -4331,22 +4417,37 @@ const buildDocumentStyle = ({
4331
4417
  justify-content: ${spreadPosition === `left` ? `flex-end` : spreadPosition === `right` ? `flex-start` : `center`};
4332
4418
  ` : ``}
4333
4419
  }
4334
- ${``}
4420
+ ${/*
4421
+ might be html * but it does mess up things like figure if so.
4422
+ check accessible_epub_3
4423
+ */
4424
+ ``}
4335
4425
  html, body {
4336
4426
  height: 100%;
4337
4427
  width: 100%;
4338
4428
  }
4339
- ${``}
4429
+ ${/*
4430
+ This one is important for preventing 100% img to resize above
4431
+ current width. Especially needed for cbz conversion
4432
+ */
4433
+ ``}
4340
4434
  html, body {
4341
4435
  -max-width: ${columnWidth}px !important;
4342
4436
  }
4343
- ${``}
4437
+ ${/*
4438
+ * @see https://hammerjs.github.io/touch-action/
4439
+ * It needs to be disabled when using free scroll
4440
+ */
4441
+ ``}
4344
4442
  html, body {
4345
4443
  ${enableTouch ? `
4346
4444
  touch-action: none
4347
4445
  ` : ``}
4348
4446
  }
4349
- ${``}
4447
+ ${/*
4448
+ prevent drag of image instead of touch on firefox
4449
+ */
4450
+ ``}
4350
4451
  img {
4351
4452
  user-select: none;
4352
4453
  -webkit-user-drag: none;
@@ -4354,9 +4455,18 @@ const buildDocumentStyle = ({
4354
4455
  -moz-user-drag: none;
4355
4456
  -o-user-drag: none;
4356
4457
  user-drag: none;
4357
- ${``}
4458
+ ${/*
4459
+ prevent weird overflow or margin. Try `block` if `flex` has weird behavior
4460
+ */
4461
+ ``}
4358
4462
  display: flex;
4359
- ${``}
4463
+ ${/*
4464
+ If the document does not have viewport, we cannot scale anything inside.
4465
+ This should never happens with a valid epub document however it will happens if
4466
+ we load .jpg, .png, etc directly in the iframe. This is expected, in this case we force
4467
+ the inner content to display correctly.
4468
+ */
4469
+ ``}
4360
4470
  ${!viewportDimensions ? `
4361
4471
  -width: 100%;
4362
4472
  max-width: 100%;
@@ -4487,7 +4597,10 @@ const buildStyleForViewportFrame = () => {
4487
4597
  height: 100%;
4488
4598
  margin: 0;
4489
4599
  }
4490
- ${``}
4600
+ ${/*
4601
+ * @see https://hammerjs.github.io/touch-action/
4602
+ */
4603
+ ``}
4491
4604
  html, body {
4492
4605
  touch-action: none;
4493
4606
  }
@@ -4495,7 +4608,10 @@ const buildStyleForViewportFrame = () => {
4495
4608
  };
4496
4609
  const buildStyleForReflowableImageOnly = ({ isScrollable, enableTouch }) => {
4497
4610
  return `
4498
- ${``}
4611
+ ${/*
4612
+ * @see https://hammerjs.github.io/touch-action/
4613
+ */
4614
+ ``}
4499
4615
  html, body {
4500
4616
  width: 100%;
4501
4617
  margin: 0;
@@ -4510,9 +4626,14 @@ const buildStyleForReflowableImageOnly = ({ isScrollable, enableTouch }) => {
4510
4626
  margin: 0;
4511
4627
  padding: 0;
4512
4628
  box-sizing: border-box;
4513
- ${``}
4629
+ ${// we make sure img spread on entire screen
4630
+ ``}
4514
4631
  width: 100%;
4515
- ${``}
4632
+ ${/**
4633
+ * line break issue
4634
+ * @see https://stackoverflow.com/questions/37869020/image-not-taking-up-the-full-height-of-container
4635
+ */
4636
+ ``}
4516
4637
  display: block;
4517
4638
  }
4518
4639
  ` : ``}
@@ -4527,13 +4648,28 @@ const buildStyleWithMultiColumn = ({
4527
4648
  parsererror {
4528
4649
  display: none !important;
4529
4650
  }
4530
- ${``}
4651
+ ${/*
4652
+ might be html * but it does mess up things like figure if so.
4653
+ check accessible_epub_3
4654
+ */
4655
+ ``}
4531
4656
  html, body {
4532
4657
  margin: 0;
4533
4658
  padding: 0 !important;
4534
4659
  -max-width: ${columnWidth}px !important;
4535
4660
  }
4536
- ${``}
4661
+ ${/*
4662
+ body {
4663
+ height: ${columnHeight}px !important;
4664
+ width: ${columnWidth}px !important;
4665
+ -margin-left: ${horizontalMargin}px !important;
4666
+ -margin-right: ${horizontalMargin}px !important;
4667
+ -margin: ${verticalMargin}px ${horizontalMargin}px !important;
4668
+ -padding-top: ${horizontalMargin}px !important;
4669
+ -padding-bottom: ${horizontalMargin}px !important;
4670
+ }
4671
+ */
4672
+ ``}
4537
4673
  body {
4538
4674
  padding: 0 !important;
4539
4675
  width: ${width}px !important;
@@ -4549,18 +4685,33 @@ const buildStyleWithMultiColumn = ({
4549
4685
  margin: 0;
4550
4686
  }
4551
4687
  body:focus-visible {
4552
- ${``}
4688
+ ${/*
4689
+ we make sure that there are no outline when we focus something inside the iframe
4690
+ */
4691
+ ``}
4553
4692
  outline: none;
4554
4693
  }
4555
- ${``}
4694
+ ${/*
4695
+ * @see https://hammerjs.github.io/touch-action/
4696
+ */
4697
+ ``}
4556
4698
  html, body {
4557
4699
  touch-action: none;
4558
4700
  }
4559
- ${``}
4701
+ ${/*
4702
+ this messes up hard, be careful with this
4703
+ */
4704
+ ``}
4560
4705
  * {
4561
4706
  -max-width: ${columnWidth}px !important;
4562
4707
  }
4563
- ${``}
4708
+ ${/*
4709
+ this is necessary to have a proper calculation when determining size
4710
+ of iframe content. If an img is using something like width:100% it would expand to
4711
+ the size of the original image and potentially gives back a wrong size (much larger)
4712
+ @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Columns/Handling_Overflow_in_Multicol
4713
+ */
4714
+ ``}
4564
4715
  img, video, audio, object, svg {
4565
4716
  max-width: 100%;
4566
4717
  max-width: ${columnWidth}px !important;
@@ -4579,7 +4730,17 @@ const buildStyleWithMultiColumn = ({
4579
4730
  box-sizing: border-box;
4580
4731
  d-max-width: ${columnWidth}px !important;
4581
4732
  }
4582
- ${``}
4733
+ ${/*
4734
+ img, video, audio, object, svg {
4735
+ max-height: ${columnHeight}px !important;
4736
+ box-sizing: border-box;
4737
+ object-fit: contain;
4738
+ -webkit-column-break-inside: avoid;
4739
+ page-break-inside: avoid;
4740
+ break-inside: avoid;
4741
+ }
4742
+ */
4743
+ ``}
4583
4744
  table {
4584
4745
  max-width: ${columnWidth}px !important;
4585
4746
  table-layout: fixed;
@@ -4776,7 +4937,11 @@ class CFI {
4776
4937
  this.isRange = false;
4777
4938
  this.opts = Object.assign(
4778
4939
  {
4940
+ // If CFI is a Simple Range, pretend it isn't
4941
+ // by parsing only the start of the range
4779
4942
  flattenRange: false,
4943
+ // Strip temporal, spatial, offset and textLocationAssertion
4944
+ // from places where they don't make sense
4780
4945
  stricter: true
4781
4946
  },
4782
4947
  opts || {}
@@ -4896,6 +5061,7 @@ class CFI {
4896
5061
  return cfi.get();
4897
5062
  }
4898
5063
  }
5064
+ // Takes two CFI paths and compares them
4899
5065
  static comparePath(a, b) {
4900
5066
  const max = Math.max(a.length, b.length);
4901
5067
  let i, cA, cB, diff;
@@ -4912,11 +5078,13 @@ class CFI {
4912
5078
  }
4913
5079
  return 0;
4914
5080
  }
5081
+ // Sort an array of CFI objects
4915
5082
  static sort(a) {
4916
5083
  a.sort((a2, b) => {
4917
5084
  return this.compare(a2, b);
4918
5085
  });
4919
5086
  }
5087
+ // Takes two CFI objects and compares them.
4920
5088
  static compare(a, b) {
4921
5089
  let oA = a.get();
4922
5090
  let oB = b.get();
@@ -4936,6 +5104,7 @@ class CFI {
4936
5104
  return this.comparePath(oA, oB);
4937
5105
  }
4938
5106
  }
5107
+ // Takes two parsed path parts (assuming path is split on '!') and compares them.
4939
5108
  static compareParts(a, b) {
4940
5109
  const max = Math.max(a.length, b.length);
4941
5110
  let i, cA, cB, diff;
@@ -4977,6 +5146,7 @@ class CFI {
4977
5146
  return str;
4978
5147
  }
4979
5148
  }
5149
+ // decode HTML/XML entities and compute length
4980
5150
  trueLength(dom, str) {
4981
5151
  return this.decodeEntities(dom, str).length;
4982
5152
  }
@@ -5242,6 +5412,9 @@ class CFI {
5242
5412
  throw new Error(`Missing child node index in CFI`);
5243
5413
  return { parsed: o, offset: i, newDoc: state === `!` };
5244
5414
  }
5415
+ // The CFI counts child nodes differently from the DOM
5416
+ // Retrieve the child of parentNode at the specified index
5417
+ // according to the CFI standard way of counting
5245
5418
  getChildNodeByCFIIndex(dom, parentNode, index, offset) {
5246
5419
  const children = parentNode.childNodes;
5247
5420
  if (!children.length)
@@ -5320,6 +5493,7 @@ class CFI {
5320
5493
  }
5321
5494
  return false;
5322
5495
  }
5496
+ // Use a Text Location Assertion to correct and offset
5323
5497
  correctOffset(dom, node, offset, assertion) {
5324
5498
  let curNode = node;
5325
5499
  let matchStr;
@@ -5417,6 +5591,15 @@ class CFI {
5417
5591
  }
5418
5592
  return o;
5419
5593
  }
5594
+ // Each part of a CFI (as separated by '!')
5595
+ // references a separate HTML/XHTML/XML document.
5596
+ // This function takes an index specifying the part
5597
+ // of the CFI and the appropriate Document or XMLDocument
5598
+ // that is referenced by the specified part of the CFI
5599
+ // and returns the URI for the document referenced by
5600
+ // the next part of the CFI
5601
+ // If the opt `ignoreIDs` is true then IDs
5602
+ // will not be used while resolving
5420
5603
  resolveURI(index, dom, opts) {
5421
5604
  opts = opts || {};
5422
5605
  if (index < 0 || index > this.parts.length - 2) {
@@ -5428,7 +5611,8 @@ class CFI {
5428
5611
  const o = this.resolveNode(index, subparts, dom, opts);
5429
5612
  let node = o.node;
5430
5613
  const tagName = node.tagName.toLowerCase();
5431
- if (tagName === `itemref` && node.parentNode.tagName.toLowerCase() === `spine`) {
5614
+ if (tagName === `itemref` && // @ts-ignore
5615
+ node.parentNode.tagName.toLowerCase() === `spine`) {
5432
5616
  const idref = node.getAttribute(`idref`);
5433
5617
  if (!idref)
5434
5618
  throw new Error(`Referenced node had not 'idref' attribute`);
@@ -5475,6 +5659,9 @@ class CFI {
5475
5659
  delete o.offset;
5476
5660
  return { ...lastPart, ...o };
5477
5661
  }
5662
+ // Takes the Document or XMLDocument for the final
5663
+ // document referenced by the CFI
5664
+ // and returns the node and offset into that node
5478
5665
  resolveLast(dom, opts) {
5479
5666
  opts = Object.assign(
5480
5667
  {
@@ -5732,7 +5919,22 @@ const createSpine = ({
5732
5919
  spineItem: beginSpineItem,
5733
5920
  spineItemIndex: beginItemIndex,
5734
5921
  pageIndex: beginPageIndex,
5735
- cfi: (lastExpectedNavigation == null ? void 0 : lastExpectedNavigation.type) === `navigate-from-cfi` && spineItemToFocus === beginSpineItem ? lastExpectedNavigation.data : data.triggeredBy === `adjust` && context.getSettings().computedPageTurnMode === `controlled` ? pagination.getInfo().beginCfi : beginItemIndex !== pagination.getInfo().beginSpineItemIndex ? cfiLocator.getRootCfi(beginSpineItem) : cfiLocator.getRootCfi(beginSpineItem),
5922
+ /**
5923
+ * Because the start of a navigation may involve animations and interactions we don't resolve heavy CFI here.
5924
+ * We do want to have certain information correct in the pagination right after a navigation (same tick) but we just
5925
+ * defer heavy non vital stuff for later.
5926
+ * There are only 4 different cfi update at this stage:
5927
+ * - navigation comes from cfi, we simply affect the cfi to the pagination
5928
+ * - navigation comes from adjustment with controlled mode, we don't update the cfi, just pass the previous one
5929
+ * - navigation comes from adjustment with free mode, we will update with root cfi if needed because we could be on new page
5930
+ * - navigation is not from adjustment, this means we are on either new page or new reading item, we use light cfi with root (no dom lookup)
5931
+ *
5932
+ * The cfi is later adjusted with heavy dom lookup once the viewport is free.
5933
+ */
5934
+ cfi: (lastExpectedNavigation == null ? void 0 : lastExpectedNavigation.type) === `navigate-from-cfi` && spineItemToFocus === beginSpineItem ? lastExpectedNavigation.data : data.triggeredBy === `adjust` && context.getSettings().computedPageTurnMode === `controlled` ? pagination.getInfo().beginCfi : beginItemIndex !== pagination.getInfo().beginSpineItemIndex ? cfiLocator.getRootCfi(beginSpineItem) : (
5935
+ /* @todo check ? */
5936
+ cfiLocator.getRootCfi(beginSpineItem)
5937
+ ),
5736
5938
  options: {
5737
5939
  isAtEndOfChapter: false
5738
5940
  }
@@ -5741,7 +5943,10 @@ const createSpine = ({
5741
5943
  spineItem: endSpineItem,
5742
5944
  spineItemIndex: endItemIndex,
5743
5945
  pageIndex: endPageIndex,
5744
- cfi: (lastExpectedNavigation == null ? void 0 : lastExpectedNavigation.type) === `navigate-from-cfi` && spineItemToFocus === endSpineItem ? lastExpectedNavigation.data : data.triggeredBy === `adjust` && context.getSettings().computedPageTurnMode === `controlled` ? pagination.getInfo().endCfi : endItemIndex !== pagination.getInfo().endSpineItemIndex ? cfiLocator.getRootCfi(endSpineItem) : cfiLocator.getRootCfi(endSpineItem),
5946
+ cfi: (lastExpectedNavigation == null ? void 0 : lastExpectedNavigation.type) === `navigate-from-cfi` && spineItemToFocus === endSpineItem ? lastExpectedNavigation.data : data.triggeredBy === `adjust` && context.getSettings().computedPageTurnMode === `controlled` ? pagination.getInfo().endCfi : endItemIndex !== pagination.getInfo().endSpineItemIndex ? cfiLocator.getRootCfi(endSpineItem) : (
5947
+ /* @todo check ? */
5948
+ cfiLocator.getRootCfi(endSpineItem)
5949
+ ),
5745
5950
  options: {
5746
5951
  isAtEndOfChapter: false
5747
5952
  }
@@ -5771,7 +5976,16 @@ const createSpine = ({
5771
5976
  takeUntil(context.$.destroy$)
5772
5977
  ).subscribe();
5773
5978
  merge(
5979
+ /**
5980
+ * We want to update content after navigation since we are at a different place.
5981
+ * We also wait for navigated items to be updated so that we have access to correct focus.
5982
+ * which is why we use this observer rather than `navigation$`.
5983
+ */
5774
5984
  itemUpdateOnNavigation$,
5985
+ /**
5986
+ * This one make sure we also listen for layout change and that we execute the code once the navigation
5987
+ * has been adjusted (whether it's needed or not).
5988
+ */
5775
5989
  navigationAdjusted$
5776
5990
  ).pipe(
5777
5991
  switchMap(() => {
@@ -5970,7 +6184,10 @@ const createSpineItemManager = ({ context }) => {
5970
6184
  const isPrePaginated = ((_a = context.getManifest()) == null ? void 0 : _a.renditionLayout) === `pre-paginated`;
5971
6185
  const isUsingFreeScroll = context.getSettings().computedPageTurnMode === `scrollable`;
5972
6186
  orderedSpineItemsSubject$.value.forEach((orderedSpineItem, index) => {
5973
- const isBeforeFocusedWithPreload = index < leftIndex && !isPrePaginated && isUsingFreeScroll ? true : index < leftIndex - numberOfAdjacentSpineItemToPreLoad;
6187
+ const isBeforeFocusedWithPreload = (
6188
+ // we never want to preload anything before on free scroll on flow because it could offset the cursor
6189
+ index < leftIndex && !isPrePaginated && isUsingFreeScroll ? true : index < leftIndex - numberOfAdjacentSpineItemToPreLoad
6190
+ );
5974
6191
  const isAfterTailWithPreload = index > rightIndex + numberOfAdjacentSpineItemToPreLoad;
5975
6192
  if (!isBeforeFocusedWithPreload && !isAfterTailWithPreload) {
5976
6193
  orderedSpineItem.loadContent();
@@ -6176,7 +6393,8 @@ const createLocationResolver$1 = ({ context }) => {
6176
6393
  var _a, _b, _c;
6177
6394
  const pageSize = context.getPageSize();
6178
6395
  const frame = (_b = (_a = spineItem.spineItemFrame) == null ? void 0 : _a.getManipulableFrame()) == null ? void 0 : _b.frame;
6179
- if (((_c = frame == null ? void 0 : frame.contentWindow) == null ? void 0 : _c.document) && frame.contentWindow.document.body !== null) {
6396
+ if (((_c = frame == null ? void 0 : frame.contentWindow) == null ? void 0 : _c.document) && // very important because it is being used by next functions
6397
+ frame.contentWindow.document.body !== null) {
6180
6398
  const { x: left, y: top } = getSpineItemPositionFromPageIndex(pageIndex, spineItem);
6181
6399
  const viewport = {
6182
6400
  left,
@@ -6746,8 +6964,14 @@ const createManualViewportNavigator = ({
6746
6964
  chapterPageNavigation$,
6747
6965
  leftPageNavigation$,
6748
6966
  rightPageNavigation$,
6967
+ // for some reason after too much item ts complains
6749
6968
  merge(cfiNavigation$, pageNavigation$)
6750
6969
  ).pipe(
6970
+ /**
6971
+ * Ideally when manually navigating we expect the navigation to be different from the previous one.
6972
+ * This is because manual navigation is not used with scroll where you can move within the same item. A manual
6973
+ * navigation would theoretically always move to different items.
6974
+ */
6751
6975
  withLatestFrom(currentNavigationSubject$),
6752
6976
  filter(([navigation, currentNavigation]) => navigator2.areNavigationDifferent(navigation, currentNavigation)),
6753
6977
  map(([navigation]) => navigation)
@@ -6922,6 +7146,9 @@ const createViewportNavigator = ({
6922
7146
  }
6923
7147
  const { x, y } = element.getBoundingClientRect();
6924
7148
  const newValue = {
7149
+ // we want to round to first decimal because it's possible to have half pixel
7150
+ // however browser engine can also gives back x.yyyy based on their precision
7151
+ // @see https://stackoverflow.com/questions/13847053/difference-between-and-math-floor for ~~
6925
7152
  x: ~~(Math.abs(x) * 10) / 10,
6926
7153
  y: ~~(Math.abs(y) * 10) / 10
6927
7154
  };
@@ -7132,6 +7359,14 @@ const createViewportNavigator = ({
7132
7359
  const animationDuration = currentEvent.animation === `snap` ? context.getSettings().computedSnapAnimationDuration : context.getSettings().computedPageTurnAnimationDuration;
7133
7360
  const pageTurnAnimation = currentEvent.animation === `snap` ? `slide` : context.getSettings().computedPageTurnAnimation;
7134
7361
  return of(currentEvent).pipe(
7362
+ /**
7363
+ * @important
7364
+ * Optimization:
7365
+ * When the adjustment does not need animation it means we want to be there as fast as possible
7366
+ * One example is when we adjust position after layout. In this case we don't want to have flicker or see
7367
+ * anything for x ms while we effectively adjust. We want it to be immediate.
7368
+ * However when user is repeatedly turning page, we can improve smoothness by delaying a bit the adjustment
7369
+ */
7135
7370
  currentEvent.shouldAnimate ? delay(1, animationFrameScheduler) : identity$1,
7136
7371
  tap((data) => {
7137
7372
  const noAdjustmentNeeded = false;
@@ -7148,6 +7383,14 @@ const createViewportNavigator = ({
7148
7383
  element.style.setProperty(`opacity`, `1`);
7149
7384
  }
7150
7385
  }),
7386
+ /**
7387
+ * @important
7388
+ * We always need to adjust the reading offset. Even if the current viewport value
7389
+ * is the same as the payload position. This is because an already running animation could
7390
+ * be active, meaning the viewport is still adjusting itself (after animation duration). So we
7391
+ * need to adjust to anchor to the payload position. This is because we use viewport computed position,
7392
+ * not the value set by `setProperty`
7393
+ */
7151
7394
  withLatestFrom(hooks$),
7152
7395
  tap(([data, hooks]) => {
7153
7396
  if (pageTurnAnimation !== `fade`) {
@@ -7186,6 +7429,12 @@ const createViewportNavigator = ({
7186
7429
  map((states) => states.every((state) => state === `end`) ? `free` : `busy`),
7187
7430
  distinctUntilChanged(),
7188
7431
  shareReplay(1),
7432
+ /**
7433
+ * @important
7434
+ * Since state$ is being updated from navigation$ and other exported streams we need it to be
7435
+ * hot so it always have the correct value no matter when someone subscribe later.
7436
+ * We cannot wait for the cold stream to start after a navigation already happened for example.
7437
+ */
7189
7438
  makeItHot
7190
7439
  );
7191
7440
  const waitForViewportFree$ = state$.pipe(
@@ -7193,6 +7442,24 @@ const createViewportNavigator = ({
7193
7442
  take$1(1)
7194
7443
  );
7195
7444
  const navigationAdjustedAfterLayout$ = spine.$.layout$.pipe(
7445
+ /**
7446
+ * @important
7447
+ * Careful with using debounce / throttle here since it can decrease user experience
7448
+ * when layout happens it can means an item before the current one has been unloaded, at current code
7449
+ * we unload and size back each item to the screen so it will have the effect of flicker for user.
7450
+ * Consider this workflow:
7451
+ * - user navigate to page 2
7452
+ * - viewport move to item 2
7453
+ * - page 1 unload and goes back from 2000px to 500px
7454
+ * - layout triggered
7455
+ * - viewport is now on an item far after item 2 because item 1 shrink (PROBLEM)
7456
+ * - sometime after viewport is adjusted back to item 2.
7457
+ *
7458
+ * Two solution to fix this issue:
7459
+ * - maybe later try to implement a different strategy and never shrink back item unless they are loaded
7460
+ * - do not use debounce / throttle and navigate back to the item right on the same tick
7461
+ */
7462
+ // debounceTime(10, animationFrameScheduler),
7196
7463
  switchMap(
7197
7464
  () => waitForViewportFree$.pipe(
7198
7465
  switchMap(() => {
@@ -7262,10 +7529,18 @@ const createLocationResolver = ({
7262
7529
  if (context.isRTL()) {
7263
7530
  return {
7264
7531
  x: leftEnd - position.x - context.getPageSize().width,
7532
+ // y: (topEnd - position.y) - context.getPageSize().height,
7265
7533
  y: Math.max(0, position.y - topStart)
7266
7534
  };
7267
7535
  }
7268
7536
  return {
7537
+ /**
7538
+ * when using spread the item could be on the right and therefore will be negative
7539
+ * @example
7540
+ * 400 (position = viewport), page of 200
7541
+ * 400 - 600 = -200.
7542
+ * However we can assume we are at 0, because we in fact can see the beginning of the item
7543
+ */
7269
7544
  x: Math.max(0, position.x - leftStart),
7270
7545
  y: Math.max(0, position.y - topStart)
7271
7546
  };
@@ -7277,6 +7552,7 @@ const createLocationResolver = ({
7277
7552
  if (context.isRTL()) {
7278
7553
  return {
7279
7554
  x: leftEnd - spineItemPosition.x - context.getPageSize().width,
7555
+ // y: (topEnd - spineItemPosition.y) - context.getPageSize().height,
7280
7556
  y: topStart + spineItemPosition.y
7281
7557
  };
7282
7558
  }
@@ -7617,10 +7893,23 @@ const createReader = ({ containerElement, hooks: initialHooks, ...settings }) =>
7617
7893
  destroy,
7618
7894
  setSettings: context.setSettings,
7619
7895
  settings$: context.$.settings$,
7896
+ /**
7897
+ * @important
7898
+ * BehaviorSubject
7899
+ */
7620
7900
  pagination$: pagination.$.info$,
7621
7901
  $: {
7622
7902
  state$: stateSubject$.asObservable(),
7903
+ /**
7904
+ * Dispatched when the reader has loaded a book and is displayed a book.
7905
+ * Using navigation API and getting information about current content will
7906
+ * have an effect.
7907
+ * It can typically be used to hide a loading indicator.
7908
+ */
7623
7909
  ready$: readySubject$.asObservable(),
7910
+ /**
7911
+ * Dispatched when a change in selection happens
7912
+ */
7624
7913
  selection$: selectionSubject$.asObservable(),
7625
7914
  viewportState$: viewportNavigator.$.state$,
7626
7915
  layout$: spine.$.layout$,
@@ -7872,6 +8161,10 @@ const resourcesEnhancer = (next) => (options) => {
7872
8161
  };
7873
8162
  return {
7874
8163
  ...reader,
8164
+ // $: {
8165
+ // ...reader.$,
8166
+ // errors$: merge(reader.$.errors$, errorsSubject$.asObservable())
8167
+ // },
7875
8168
  destroy,
7876
8169
  load
7877
8170
  };
@@ -8010,7 +8303,11 @@ const accessibilityEnhancer = (next) => (options) => {
8010
8303
  `prose-reader-accessibility`,
8011
8304
  `
8012
8305
  :focus-visible {
8013
- ${``}
8306
+ ${/*
8307
+ Some epubs remove the outline, this is not good practice since it reduce accessibility.
8308
+ We will try to restore it by force.
8309
+ */
8310
+ ``}
8014
8311
  outline: -webkit-focus-ring-color auto 1px;
8015
8312
  }
8016
8313
  `
@@ -8165,23 +8462,27 @@ const defaultLoadingElementCreate = ({ container, item }) => {
8165
8462
  container.appendChild(detailsElement);
8166
8463
  return container;
8167
8464
  };
8168
- const createReaderWithEnhancers = loadingEnhancer(
8169
- webkitEnhancer(
8170
- fontsEnhancer(
8171
- linksEnhancer(
8172
- accessibilityEnhancer(
8173
- resourcesEnhancer(
8174
- utilsEnhancer(
8175
- layoutEnhancer(
8176
- zoomEnhancer(
8177
- mediaEnhancer(
8178
- chromeEnhancer(
8179
- navigationEnhancer(
8180
- themeEnhancer(
8181
- hotkeysEnhancer(
8182
- paginationEnhancer(
8183
- progressionEnhancer(
8184
- createReader
8465
+ const createReaderWithEnhancers = (
8466
+ //__
8467
+ loadingEnhancer(
8468
+ webkitEnhancer(
8469
+ fontsEnhancer(
8470
+ linksEnhancer(
8471
+ accessibilityEnhancer(
8472
+ resourcesEnhancer(
8473
+ utilsEnhancer(
8474
+ layoutEnhancer(
8475
+ zoomEnhancer(
8476
+ mediaEnhancer(
8477
+ chromeEnhancer(
8478
+ navigationEnhancer(
8479
+ themeEnhancer(
8480
+ hotkeysEnhancer(
8481
+ paginationEnhancer(
8482
+ progressionEnhancer(
8483
+ // __
8484
+ createReader
8485
+ )
8185
8486
  )
8186
8487
  )
8187
8488
  )