@prose-reader/core 1.14.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.
@@ -2127,7 +2127,15 @@
2127
2127
  });
2128
2128
  const reader = next(options);
2129
2129
  const getStyle = () => `
2130
- ${``}
2130
+ ${/*
2131
+ Ideally, we would like to use !important but it could break publisher specific
2132
+ cases.
2133
+ Also right now we do not apply it to * since it would also break publisher
2134
+ more specific scaling down the tree.
2135
+
2136
+ body *:not([class^="mjx-"]) {
2137
+ */
2138
+ ``}
2131
2139
  body {
2132
2140
  ${settingsSubject$.value.fontScale !== 1 ? `font-size: ${settingsSubject$.value.fontScale}em !important;` : ``}
2133
2141
  ${settingsSubject$.value.lineHeight !== `publisher` ? `line-height: ${settingsSubject$.value.lineHeight} !important;` : ``}
@@ -2169,6 +2177,9 @@
2169
2177
  );
2170
2178
  return {
2171
2179
  ...reader,
2180
+ /**
2181
+ * Absorb current enhancer settings and passthrough the rest to reader
2182
+ */
2172
2183
  setSettings: (settings) => {
2173
2184
  const {
2174
2185
  fontJustification: fontJustification2 = settingsSubject$.value.fontJustification,
@@ -2182,6 +2193,9 @@
2182
2193
  }
2183
2194
  reader.setSettings(passthroughSettings);
2184
2195
  },
2196
+ /**
2197
+ * Combine reader settings with enhancer settings
2198
+ */
2185
2199
  settings$
2186
2200
  };
2187
2201
  };
@@ -2449,6 +2463,7 @@
2449
2463
  };
2450
2464
  };
2451
2465
  const createReport = (namespace) => ({
2466
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2452
2467
  log: (...data) => {
2453
2468
  if (window.__PROSE_READER_DEBUG) {
2454
2469
  if (namespace)
@@ -2457,6 +2472,7 @@
2457
2472
  console.log(wrap(ROOT_NAMESPACE), ...data);
2458
2473
  }
2459
2474
  },
2475
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2460
2476
  warn: (...data) => {
2461
2477
  if (window.__PROSE_READER_DEBUG) {
2462
2478
  if (namespace)
@@ -2465,9 +2481,22 @@
2465
2481
  console.warn(wrap(ROOT_NAMESPACE), ...data);
2466
2482
  }
2467
2483
  },
2484
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2468
2485
  error: (...data) => {
2469
2486
  console.error(...data);
2470
2487
  },
2488
+ // time: (label?: string | undefined) => {
2489
+ // if (window.__PROSE_READER_DEBUG) {
2490
+ // // eslint-disable-next-line no-console
2491
+ // console.time(`[prose-reader] [metric] ${label}`);
2492
+ // }
2493
+ // },
2494
+ // timeEnd: (label?: string | undefined) => {
2495
+ // if (window.__PROSE_READER_DEBUG) {
2496
+ // // eslint-disable-next-line no-console
2497
+ // console.timeEnd(`[prose-reader] [metric] ${label}`);
2498
+ // }
2499
+ // },
2471
2500
  time,
2472
2501
  logMetric: (performanceEntry, targetDuration = 0) => {
2473
2502
  if (window.__PROSE_READER_DEBUG) {
@@ -2481,6 +2510,7 @@
2481
2510
  }
2482
2511
  }
2483
2512
  },
2513
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2484
2514
  measurePerformance: (name, targetDuration = 10, functionToMeasure, { disable } = {}) => {
2485
2515
  if (disable || !window.__PROSE_READER_DEBUG)
2486
2516
  return functionToMeasure;
@@ -2675,15 +2705,37 @@
2675
2705
  beginPageIndexInChapter: paginationInfo.beginPageIndex,
2676
2706
  beginNumberOfPagesInChapter: paginationInfo.beginNumberOfPages,
2677
2707
  beginChapterInfo: beginItem ? chaptersInfo[beginItem.item.id] : void 0,
2708
+ // chapterIndex: number;
2709
+ // pages: number;
2710
+ // pageIndexInBook: number;
2711
+ // pageIndexInChapter: number;
2712
+ // pagesOfChapter: number;
2713
+ // pagePercentageInChapter: number;
2714
+ // offsetPercentageInChapter: number;
2715
+ // domIndex: number;
2716
+ // charOffset: number;
2717
+ // serializeString?: string;
2678
2718
  beginSpineItemIndex: paginationInfo.beginSpineItemIndex,
2719
+ // spineItemPath: beginItem?.item.path,
2720
+ // spineItemId: beginItem?.item.id,
2679
2721
  beginCfi: paginationInfo.beginCfi,
2680
2722
  beginSpineItemReadingDirection: beginItem == null ? void 0 : beginItem.getReadingDirection(),
2681
2723
  endChapterInfo: endItem ? chaptersInfo[endItem.item.id] : void 0,
2682
2724
  endPageIndexInChapter: paginationInfo.endPageIndex,
2683
2725
  endNumberOfPagesInChapter: paginationInfo.endNumberOfPages,
2684
2726
  endSpineItemIndex: paginationInfo.endSpineItemIndex,
2727
+ // spineItemPath: endItem?.item.path,
2728
+ // spineItemId: endItem?.item.id,
2685
2729
  endSpineItemReadingDirection: endItem == null ? void 0 : endItem.getReadingDirection(),
2686
2730
  endCfi: paginationInfo.endCfi,
2731
+ // end: ReadingLocation;
2732
+ // spineItemReadingDirection: focusedSpineItem?.getReadingDirection(),
2733
+ /**
2734
+ * This percentage is based of the weight (kb) of every items and the number of pages.
2735
+ * It is not accurate but gives a general good idea of the overall progress.
2736
+ * It is recommended to use this progress only for reflow books. For pre-paginated books
2737
+ * the number of pages and current index can be used instead since 1 page = 1 chapter.
2738
+ */
2687
2739
  percentageEstimateOfBook: endItem ? reader.progression.getPercentageEstimate(
2688
2740
  context,
2689
2741
  paginationInfo.endSpineItemIndex ?? 0,
@@ -2693,6 +2745,11 @@
2693
2745
  endItem
2694
2746
  ) : 0,
2695
2747
  isUsingSpread: context.shouldDisplaySpread()
2748
+ // chaptersOfBook: number;
2749
+ // chapter: string;
2750
+ // hasNextChapter: (reader.spine.spineItemIndex || 0) < (manifest.readingOrder.length - 1),
2751
+ // hasPreviousChapter: (reader.spine.spineItemIndex || 0) < (manifest.readingOrder.length - 1),
2752
+ // numberOfSpineItems: context.getManifest()?.readingOrder.length,
2696
2753
  };
2697
2754
  };
2698
2755
  const getSpineItemNumberOfPages = (spineItem) => {
@@ -2732,6 +2789,9 @@
2732
2789
  const numberOfPagesPerItems = getNumberOfPagesPerItems();
2733
2790
  return {
2734
2791
  numberOfPagesPerItems,
2792
+ /**
2793
+ * This may be not accurate for reflowable due to dynamic load / unload.
2794
+ */
2735
2795
  numberOfTotalPages: numberOfPagesPerItems.reduce((acc, numberOfPagesForItem) => acc + numberOfPagesForItem, 0)
2736
2796
  };
2737
2797
  }),
@@ -2762,28 +2822,34 @@
2762
2822
  const buildChapterInfoFromSpineItem = (manifest, item) => {
2763
2823
  var _a;
2764
2824
  const { href } = item;
2765
- return getChapterInfo(href, ((_a = manifest.nav) == null ? void 0 : _a.toc) ?? []);
2825
+ return getChapterInfo(href, ((_a = manifest.nav) == null ? void 0 : _a.toc) ?? [], manifest);
2766
2826
  };
2767
- const getChapterInfo = (href, tocItems) => {
2768
- return tocItems.reduce((acc, tocItem) => {
2769
- const indexOfHash = tocItem.href.indexOf(`#`);
2770
- const tocItemPathWithoutAnchor = indexOfHash > 0 ? tocItem.href.substr(0, indexOfHash) : tocItem.href;
2827
+ const getChapterInfo = (href, tocItem, manifest) => {
2828
+ const spineItemIndex = manifest.spineItems.findIndex((item) => item.href === href);
2829
+ return tocItem.reduce((acc, tocItem2) => {
2830
+ const indexOfHash = tocItem2.href.indexOf(`#`);
2831
+ const tocItemPathWithoutAnchor = indexOfHash > 0 ? tocItem2.href.substr(0, indexOfHash) : tocItem2.href;
2771
2832
  const tocItemHrefWithoutFilename = tocItemPathWithoutAnchor.substring(0, tocItemPathWithoutAnchor.lastIndexOf("/"));
2772
2833
  const hrefWithoutFilename = href.substring(0, href.lastIndexOf("/"));
2773
2834
  const hrefIsChapterHref = href.endsWith(tocItemPathWithoutAnchor);
2774
2835
  const hrefIsWithinChapter = hrefWithoutFilename !== "" && hrefWithoutFilename.endsWith(tocItemHrefWithoutFilename);
2775
- if (hrefIsChapterHref || hrefIsWithinChapter) {
2836
+ const isPossibleTocItemCandidate = hrefIsChapterHref || hrefIsWithinChapter;
2837
+ if (isPossibleTocItemCandidate) {
2838
+ const spineItemIndexOfPossibleCandidate = manifest.spineItems.findIndex((item) => item.href === tocItem2.href);
2839
+ const spineItemIsBeforeThisTocItem = spineItemIndex < spineItemIndexOfPossibleCandidate;
2840
+ if (spineItemIsBeforeThisTocItem)
2841
+ return acc;
2776
2842
  return {
2777
- title: tocItem.title,
2778
- path: tocItem.path
2843
+ title: tocItem2.title,
2844
+ path: tocItem2.path
2779
2845
  };
2780
2846
  }
2781
- const subInfo = getChapterInfo(href, tocItem.contents);
2847
+ const subInfo = getChapterInfo(href, tocItem2.contents, manifest);
2782
2848
  if (subInfo) {
2783
2849
  return {
2784
2850
  subChapter: subInfo,
2785
- title: tocItem.title,
2786
- path: tocItem.path
2851
+ title: tocItem2.title,
2852
+ path: tocItem2.path
2787
2853
  };
2788
2854
  }
2789
2855
  return acc;
@@ -2816,7 +2882,11 @@
2816
2882
  }
2817
2883
  ${(foundTheme == null ? void 0 : foundTheme.foregroundColor) ? `
2818
2884
  body * {
2819
- ${``}
2885
+ ${/*
2886
+ Ideally, we would like to use !important but it could break publisher specific
2887
+ cases
2888
+ */
2889
+ ``}
2820
2890
  color: ${foundTheme.foregroundColor};
2821
2891
  }
2822
2892
  ` : ``}
@@ -3414,6 +3484,8 @@
3414
3484
  take(1)
3415
3485
  );
3416
3486
  const unload$ = unloadSubject$.asObservable().pipe(
3487
+ // @todo remove iframe when viewport is free
3488
+ // @todo use takeUntil(load$) when it's the case to cancel
3417
3489
  withLatestFrom(frameElementSubject$),
3418
3490
  filter(([_, frame]) => !!frame),
3419
3491
  map(([, frame]) => {
@@ -3434,13 +3506,16 @@
3434
3506
  const load$ = loadSubject$.asObservable().pipe(
3435
3507
  withLatestFrom(isLoadedSubject$),
3436
3508
  filter(([_, isLoaded]) => !isLoaded),
3509
+ // let's ignore later load as long as the first one still runs
3437
3510
  exhaustMap(() => {
3438
3511
  return createFrame$().pipe(
3439
3512
  mergeMap((frame) => waitForViewportFree$.pipe(map(() => frame))),
3440
3513
  mergeMap((frame) => {
3441
3514
  parent.appendChild(frame);
3442
3515
  frameElementSubject$.next(frame);
3443
- 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)))) {
3516
+ if (!fetchResource && item.href.startsWith(window.location.origin) && // we have an encoding and it's a valid html
3517
+ (item.mediaType && [`application/xhtml+xml`, `application/xml`, `text/html`, `text/xml`].includes(item.mediaType) || // no encoding ? then try to detect html
3518
+ !item.mediaType && ITEM_EXTENSION_VALID_FOR_FRAME_SRC.some((extension) => item.href.endsWith(extension)))) {
3444
3519
  frame == null ? void 0 : frame.setAttribute(`src`, item.href);
3445
3520
  return rxjs.of(frame);
3446
3521
  } else {
@@ -3496,6 +3571,7 @@
3496
3571
  })
3497
3572
  );
3498
3573
  }),
3574
+ // we stop loading as soon as unload is requested
3499
3575
  takeUntil(unloadSubject$)
3500
3576
  );
3501
3577
  }),
@@ -3622,6 +3698,12 @@
3622
3698
  getHtmlFromResource,
3623
3699
  load,
3624
3700
  unload,
3701
+ /**
3702
+ * Upward layout is used when the parent wants to manipulate the iframe without triggering
3703
+ * `layout` event. This is a particular case needed for iframe because the parent can layout following
3704
+ * an iframe `layout` event. Because the parent `layout` may change some of iframe properties we do not
3705
+ * want the iframe to trigger a new `layout` even and have infinite loop.
3706
+ */
3625
3707
  staticLayout: (size) => {
3626
3708
  const frame = frameElement$.getValue();
3627
3709
  if (frame) {
@@ -3632,6 +3714,8 @@
3632
3714
  }
3633
3715
  }
3634
3716
  },
3717
+ // @todo block access, only public API to manipulate / get information (in order to memo / optimize)
3718
+ // manipulate() with cb and return boolean whether re-layout or not
3635
3719
  getManipulableFrame,
3636
3720
  getReadingDirection: () => {
3637
3721
  var _a;
@@ -3653,6 +3737,10 @@
3653
3737
  loaded$,
3654
3738
  ready$,
3655
3739
  isReady$: isReadySubject$.asObservable(),
3740
+ /**
3741
+ * This is used as upstream layout change. This event is being listened to by upper app
3742
+ * in order to layout again and adjust every element based on the new content.
3743
+ */
3656
3744
  contentLayoutChange$
3657
3745
  }
3658
3746
  };
@@ -3931,6 +4019,8 @@
3931
4019
  const rect = containerElement.getBoundingClientRect();
3932
4020
  const normalizedValues = {
3933
4021
  ...rect,
4022
+ // we want to round to first decimal because it's possible to have half pixel
4023
+ // however browser engine can also gives back x.yyyy based on their precision
3934
4024
  width: Math.round(rect.width * 10) / 10,
3935
4025
  height: Math.round(rect.height * 10) / 10
3936
4026
  };
@@ -4010,6 +4100,8 @@
4010
4100
  return {
4011
4101
  columnHeight,
4012
4102
  columnWidth,
4103
+ // horizontalMargin,
4104
+ // verticalMargin,
4013
4105
  width
4014
4106
  };
4015
4107
  };
@@ -4327,22 +4419,37 @@
4327
4419
  justify-content: ${spreadPosition === `left` ? `flex-end` : spreadPosition === `right` ? `flex-start` : `center`};
4328
4420
  ` : ``}
4329
4421
  }
4330
- ${``}
4422
+ ${/*
4423
+ might be html * but it does mess up things like figure if so.
4424
+ check accessible_epub_3
4425
+ */
4426
+ ``}
4331
4427
  html, body {
4332
4428
  height: 100%;
4333
4429
  width: 100%;
4334
4430
  }
4335
- ${``}
4431
+ ${/*
4432
+ This one is important for preventing 100% img to resize above
4433
+ current width. Especially needed for cbz conversion
4434
+ */
4435
+ ``}
4336
4436
  html, body {
4337
4437
  -max-width: ${columnWidth}px !important;
4338
4438
  }
4339
- ${``}
4439
+ ${/*
4440
+ * @see https://hammerjs.github.io/touch-action/
4441
+ * It needs to be disabled when using free scroll
4442
+ */
4443
+ ``}
4340
4444
  html, body {
4341
4445
  ${enableTouch ? `
4342
4446
  touch-action: none
4343
4447
  ` : ``}
4344
4448
  }
4345
- ${``}
4449
+ ${/*
4450
+ prevent drag of image instead of touch on firefox
4451
+ */
4452
+ ``}
4346
4453
  img {
4347
4454
  user-select: none;
4348
4455
  -webkit-user-drag: none;
@@ -4350,9 +4457,18 @@
4350
4457
  -moz-user-drag: none;
4351
4458
  -o-user-drag: none;
4352
4459
  user-drag: none;
4353
- ${``}
4460
+ ${/*
4461
+ prevent weird overflow or margin. Try `block` if `flex` has weird behavior
4462
+ */
4463
+ ``}
4354
4464
  display: flex;
4355
- ${``}
4465
+ ${/*
4466
+ If the document does not have viewport, we cannot scale anything inside.
4467
+ This should never happens with a valid epub document however it will happens if
4468
+ we load .jpg, .png, etc directly in the iframe. This is expected, in this case we force
4469
+ the inner content to display correctly.
4470
+ */
4471
+ ``}
4356
4472
  ${!viewportDimensions ? `
4357
4473
  -width: 100%;
4358
4474
  max-width: 100%;
@@ -4483,7 +4599,10 @@
4483
4599
  height: 100%;
4484
4600
  margin: 0;
4485
4601
  }
4486
- ${``}
4602
+ ${/*
4603
+ * @see https://hammerjs.github.io/touch-action/
4604
+ */
4605
+ ``}
4487
4606
  html, body {
4488
4607
  touch-action: none;
4489
4608
  }
@@ -4491,7 +4610,10 @@
4491
4610
  };
4492
4611
  const buildStyleForReflowableImageOnly = ({ isScrollable, enableTouch }) => {
4493
4612
  return `
4494
- ${``}
4613
+ ${/*
4614
+ * @see https://hammerjs.github.io/touch-action/
4615
+ */
4616
+ ``}
4495
4617
  html, body {
4496
4618
  width: 100%;
4497
4619
  margin: 0;
@@ -4506,9 +4628,14 @@
4506
4628
  margin: 0;
4507
4629
  padding: 0;
4508
4630
  box-sizing: border-box;
4509
- ${``}
4631
+ ${// we make sure img spread on entire screen
4632
+ ``}
4510
4633
  width: 100%;
4511
- ${``}
4634
+ ${/**
4635
+ * line break issue
4636
+ * @see https://stackoverflow.com/questions/37869020/image-not-taking-up-the-full-height-of-container
4637
+ */
4638
+ ``}
4512
4639
  display: block;
4513
4640
  }
4514
4641
  ` : ``}
@@ -4523,13 +4650,28 @@
4523
4650
  parsererror {
4524
4651
  display: none !important;
4525
4652
  }
4526
- ${``}
4653
+ ${/*
4654
+ might be html * but it does mess up things like figure if so.
4655
+ check accessible_epub_3
4656
+ */
4657
+ ``}
4527
4658
  html, body {
4528
4659
  margin: 0;
4529
4660
  padding: 0 !important;
4530
4661
  -max-width: ${columnWidth}px !important;
4531
4662
  }
4532
- ${``}
4663
+ ${/*
4664
+ body {
4665
+ height: ${columnHeight}px !important;
4666
+ width: ${columnWidth}px !important;
4667
+ -margin-left: ${horizontalMargin}px !important;
4668
+ -margin-right: ${horizontalMargin}px !important;
4669
+ -margin: ${verticalMargin}px ${horizontalMargin}px !important;
4670
+ -padding-top: ${horizontalMargin}px !important;
4671
+ -padding-bottom: ${horizontalMargin}px !important;
4672
+ }
4673
+ */
4674
+ ``}
4533
4675
  body {
4534
4676
  padding: 0 !important;
4535
4677
  width: ${width}px !important;
@@ -4545,18 +4687,33 @@
4545
4687
  margin: 0;
4546
4688
  }
4547
4689
  body:focus-visible {
4548
- ${``}
4690
+ ${/*
4691
+ we make sure that there are no outline when we focus something inside the iframe
4692
+ */
4693
+ ``}
4549
4694
  outline: none;
4550
4695
  }
4551
- ${``}
4696
+ ${/*
4697
+ * @see https://hammerjs.github.io/touch-action/
4698
+ */
4699
+ ``}
4552
4700
  html, body {
4553
4701
  touch-action: none;
4554
4702
  }
4555
- ${``}
4703
+ ${/*
4704
+ this messes up hard, be careful with this
4705
+ */
4706
+ ``}
4556
4707
  * {
4557
4708
  -max-width: ${columnWidth}px !important;
4558
4709
  }
4559
- ${``}
4710
+ ${/*
4711
+ this is necessary to have a proper calculation when determining size
4712
+ of iframe content. If an img is using something like width:100% it would expand to
4713
+ the size of the original image and potentially gives back a wrong size (much larger)
4714
+ @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Columns/Handling_Overflow_in_Multicol
4715
+ */
4716
+ ``}
4560
4717
  img, video, audio, object, svg {
4561
4718
  max-width: 100%;
4562
4719
  max-width: ${columnWidth}px !important;
@@ -4575,7 +4732,17 @@
4575
4732
  box-sizing: border-box;
4576
4733
  d-max-width: ${columnWidth}px !important;
4577
4734
  }
4578
- ${``}
4735
+ ${/*
4736
+ img, video, audio, object, svg {
4737
+ max-height: ${columnHeight}px !important;
4738
+ box-sizing: border-box;
4739
+ object-fit: contain;
4740
+ -webkit-column-break-inside: avoid;
4741
+ page-break-inside: avoid;
4742
+ break-inside: avoid;
4743
+ }
4744
+ */
4745
+ ``}
4579
4746
  table {
4580
4747
  max-width: ${columnWidth}px !important;
4581
4748
  table-layout: fixed;
@@ -4772,7 +4939,11 @@
4772
4939
  this.isRange = false;
4773
4940
  this.opts = Object.assign(
4774
4941
  {
4942
+ // If CFI is a Simple Range, pretend it isn't
4943
+ // by parsing only the start of the range
4775
4944
  flattenRange: false,
4945
+ // Strip temporal, spatial, offset and textLocationAssertion
4946
+ // from places where they don't make sense
4776
4947
  stricter: true
4777
4948
  },
4778
4949
  opts || {}
@@ -4892,6 +5063,7 @@
4892
5063
  return cfi.get();
4893
5064
  }
4894
5065
  }
5066
+ // Takes two CFI paths and compares them
4895
5067
  static comparePath(a, b) {
4896
5068
  const max = Math.max(a.length, b.length);
4897
5069
  let i, cA, cB, diff;
@@ -4908,11 +5080,13 @@
4908
5080
  }
4909
5081
  return 0;
4910
5082
  }
5083
+ // Sort an array of CFI objects
4911
5084
  static sort(a) {
4912
5085
  a.sort((a2, b) => {
4913
5086
  return this.compare(a2, b);
4914
5087
  });
4915
5088
  }
5089
+ // Takes two CFI objects and compares them.
4916
5090
  static compare(a, b) {
4917
5091
  let oA = a.get();
4918
5092
  let oB = b.get();
@@ -4932,6 +5106,7 @@
4932
5106
  return this.comparePath(oA, oB);
4933
5107
  }
4934
5108
  }
5109
+ // Takes two parsed path parts (assuming path is split on '!') and compares them.
4935
5110
  static compareParts(a, b) {
4936
5111
  const max = Math.max(a.length, b.length);
4937
5112
  let i, cA, cB, diff;
@@ -4973,6 +5148,7 @@
4973
5148
  return str;
4974
5149
  }
4975
5150
  }
5151
+ // decode HTML/XML entities and compute length
4976
5152
  trueLength(dom, str) {
4977
5153
  return this.decodeEntities(dom, str).length;
4978
5154
  }
@@ -5238,6 +5414,9 @@
5238
5414
  throw new Error(`Missing child node index in CFI`);
5239
5415
  return { parsed: o, offset: i, newDoc: state === `!` };
5240
5416
  }
5417
+ // The CFI counts child nodes differently from the DOM
5418
+ // Retrieve the child of parentNode at the specified index
5419
+ // according to the CFI standard way of counting
5241
5420
  getChildNodeByCFIIndex(dom, parentNode, index, offset) {
5242
5421
  const children = parentNode.childNodes;
5243
5422
  if (!children.length)
@@ -5316,6 +5495,7 @@
5316
5495
  }
5317
5496
  return false;
5318
5497
  }
5498
+ // Use a Text Location Assertion to correct and offset
5319
5499
  correctOffset(dom, node, offset, assertion) {
5320
5500
  let curNode = node;
5321
5501
  let matchStr;
@@ -5413,6 +5593,15 @@
5413
5593
  }
5414
5594
  return o;
5415
5595
  }
5596
+ // Each part of a CFI (as separated by '!')
5597
+ // references a separate HTML/XHTML/XML document.
5598
+ // This function takes an index specifying the part
5599
+ // of the CFI and the appropriate Document or XMLDocument
5600
+ // that is referenced by the specified part of the CFI
5601
+ // and returns the URI for the document referenced by
5602
+ // the next part of the CFI
5603
+ // If the opt `ignoreIDs` is true then IDs
5604
+ // will not be used while resolving
5416
5605
  resolveURI(index, dom, opts) {
5417
5606
  opts = opts || {};
5418
5607
  if (index < 0 || index > this.parts.length - 2) {
@@ -5424,7 +5613,8 @@
5424
5613
  const o = this.resolveNode(index, subparts, dom, opts);
5425
5614
  let node = o.node;
5426
5615
  const tagName = node.tagName.toLowerCase();
5427
- if (tagName === `itemref` && node.parentNode.tagName.toLowerCase() === `spine`) {
5616
+ if (tagName === `itemref` && // @ts-ignore
5617
+ node.parentNode.tagName.toLowerCase() === `spine`) {
5428
5618
  const idref = node.getAttribute(`idref`);
5429
5619
  if (!idref)
5430
5620
  throw new Error(`Referenced node had not 'idref' attribute`);
@@ -5471,6 +5661,9 @@
5471
5661
  delete o.offset;
5472
5662
  return { ...lastPart, ...o };
5473
5663
  }
5664
+ // Takes the Document or XMLDocument for the final
5665
+ // document referenced by the CFI
5666
+ // and returns the node and offset into that node
5474
5667
  resolveLast(dom, opts) {
5475
5668
  opts = Object.assign(
5476
5669
  {
@@ -5728,7 +5921,22 @@
5728
5921
  spineItem: beginSpineItem,
5729
5922
  spineItemIndex: beginItemIndex,
5730
5923
  pageIndex: beginPageIndex,
5731
- 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),
5924
+ /**
5925
+ * Because the start of a navigation may involve animations and interactions we don't resolve heavy CFI here.
5926
+ * We do want to have certain information correct in the pagination right after a navigation (same tick) but we just
5927
+ * defer heavy non vital stuff for later.
5928
+ * There are only 4 different cfi update at this stage:
5929
+ * - navigation comes from cfi, we simply affect the cfi to the pagination
5930
+ * - navigation comes from adjustment with controlled mode, we don't update the cfi, just pass the previous one
5931
+ * - navigation comes from adjustment with free mode, we will update with root cfi if needed because we could be on new page
5932
+ * - 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)
5933
+ *
5934
+ * The cfi is later adjusted with heavy dom lookup once the viewport is free.
5935
+ */
5936
+ 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) : (
5937
+ /* @todo check ? */
5938
+ cfiLocator.getRootCfi(beginSpineItem)
5939
+ ),
5732
5940
  options: {
5733
5941
  isAtEndOfChapter: false
5734
5942
  }
@@ -5737,7 +5945,10 @@
5737
5945
  spineItem: endSpineItem,
5738
5946
  spineItemIndex: endItemIndex,
5739
5947
  pageIndex: endPageIndex,
5740
- 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),
5948
+ 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) : (
5949
+ /* @todo check ? */
5950
+ cfiLocator.getRootCfi(endSpineItem)
5951
+ ),
5741
5952
  options: {
5742
5953
  isAtEndOfChapter: false
5743
5954
  }
@@ -5767,7 +5978,16 @@
5767
5978
  takeUntil(context.$.destroy$)
5768
5979
  ).subscribe();
5769
5980
  rxjs.merge(
5981
+ /**
5982
+ * We want to update content after navigation since we are at a different place.
5983
+ * We also wait for navigated items to be updated so that we have access to correct focus.
5984
+ * which is why we use this observer rather than `navigation$`.
5985
+ */
5770
5986
  itemUpdateOnNavigation$,
5987
+ /**
5988
+ * This one make sure we also listen for layout change and that we execute the code once the navigation
5989
+ * has been adjusted (whether it's needed or not).
5990
+ */
5771
5991
  navigationAdjusted$
5772
5992
  ).pipe(
5773
5993
  switchMap(() => {
@@ -5966,7 +6186,10 @@
5966
6186
  const isPrePaginated = ((_a = context.getManifest()) == null ? void 0 : _a.renditionLayout) === `pre-paginated`;
5967
6187
  const isUsingFreeScroll = context.getSettings().computedPageTurnMode === `scrollable`;
5968
6188
  orderedSpineItemsSubject$.value.forEach((orderedSpineItem, index) => {
5969
- const isBeforeFocusedWithPreload = index < leftIndex && !isPrePaginated && isUsingFreeScroll ? true : index < leftIndex - numberOfAdjacentSpineItemToPreLoad;
6189
+ const isBeforeFocusedWithPreload = (
6190
+ // we never want to preload anything before on free scroll on flow because it could offset the cursor
6191
+ index < leftIndex && !isPrePaginated && isUsingFreeScroll ? true : index < leftIndex - numberOfAdjacentSpineItemToPreLoad
6192
+ );
5970
6193
  const isAfterTailWithPreload = index > rightIndex + numberOfAdjacentSpineItemToPreLoad;
5971
6194
  if (!isBeforeFocusedWithPreload && !isAfterTailWithPreload) {
5972
6195
  orderedSpineItem.loadContent();
@@ -6172,7 +6395,8 @@
6172
6395
  var _a, _b, _c;
6173
6396
  const pageSize = context.getPageSize();
6174
6397
  const frame = (_b = (_a = spineItem.spineItemFrame) == null ? void 0 : _a.getManipulableFrame()) == null ? void 0 : _b.frame;
6175
- if (((_c = frame == null ? void 0 : frame.contentWindow) == null ? void 0 : _c.document) && frame.contentWindow.document.body !== null) {
6398
+ if (((_c = frame == null ? void 0 : frame.contentWindow) == null ? void 0 : _c.document) && // very important because it is being used by next functions
6399
+ frame.contentWindow.document.body !== null) {
6176
6400
  const { x: left, y: top } = getSpineItemPositionFromPageIndex(pageIndex, spineItem);
6177
6401
  const viewport = {
6178
6402
  left,
@@ -6742,8 +6966,14 @@
6742
6966
  chapterPageNavigation$,
6743
6967
  leftPageNavigation$,
6744
6968
  rightPageNavigation$,
6969
+ // for some reason after too much item ts complains
6745
6970
  rxjs.merge(cfiNavigation$, pageNavigation$)
6746
6971
  ).pipe(
6972
+ /**
6973
+ * Ideally when manually navigating we expect the navigation to be different from the previous one.
6974
+ * This is because manual navigation is not used with scroll where you can move within the same item. A manual
6975
+ * navigation would theoretically always move to different items.
6976
+ */
6747
6977
  withLatestFrom(currentNavigationSubject$),
6748
6978
  filter(([navigation, currentNavigation]) => navigator2.areNavigationDifferent(navigation, currentNavigation)),
6749
6979
  map(([navigation]) => navigation)
@@ -6918,6 +7148,9 @@
6918
7148
  }
6919
7149
  const { x, y } = element.getBoundingClientRect();
6920
7150
  const newValue = {
7151
+ // we want to round to first decimal because it's possible to have half pixel
7152
+ // however browser engine can also gives back x.yyyy based on their precision
7153
+ // @see https://stackoverflow.com/questions/13847053/difference-between-and-math-floor for ~~
6921
7154
  x: ~~(Math.abs(x) * 10) / 10,
6922
7155
  y: ~~(Math.abs(y) * 10) / 10
6923
7156
  };
@@ -7128,6 +7361,14 @@
7128
7361
  const animationDuration = currentEvent.animation === `snap` ? context.getSettings().computedSnapAnimationDuration : context.getSettings().computedPageTurnAnimationDuration;
7129
7362
  const pageTurnAnimation = currentEvent.animation === `snap` ? `slide` : context.getSettings().computedPageTurnAnimation;
7130
7363
  return rxjs.of(currentEvent).pipe(
7364
+ /**
7365
+ * @important
7366
+ * Optimization:
7367
+ * When the adjustment does not need animation it means we want to be there as fast as possible
7368
+ * One example is when we adjust position after layout. In this case we don't want to have flicker or see
7369
+ * anything for x ms while we effectively adjust. We want it to be immediate.
7370
+ * However when user is repeatedly turning page, we can improve smoothness by delaying a bit the adjustment
7371
+ */
7131
7372
  currentEvent.shouldAnimate ? delay(1, rxjs.animationFrameScheduler) : rxjs.identity,
7132
7373
  tap((data) => {
7133
7374
  const noAdjustmentNeeded = false;
@@ -7144,6 +7385,14 @@
7144
7385
  element.style.setProperty(`opacity`, `1`);
7145
7386
  }
7146
7387
  }),
7388
+ /**
7389
+ * @important
7390
+ * We always need to adjust the reading offset. Even if the current viewport value
7391
+ * is the same as the payload position. This is because an already running animation could
7392
+ * be active, meaning the viewport is still adjusting itself (after animation duration). So we
7393
+ * need to adjust to anchor to the payload position. This is because we use viewport computed position,
7394
+ * not the value set by `setProperty`
7395
+ */
7147
7396
  withLatestFrom(hooks$),
7148
7397
  tap(([data, hooks]) => {
7149
7398
  if (pageTurnAnimation !== `fade`) {
@@ -7182,6 +7431,12 @@
7182
7431
  map((states) => states.every((state) => state === `end`) ? `free` : `busy`),
7183
7432
  distinctUntilChanged(),
7184
7433
  shareReplay(1),
7434
+ /**
7435
+ * @important
7436
+ * Since state$ is being updated from navigation$ and other exported streams we need it to be
7437
+ * hot so it always have the correct value no matter when someone subscribe later.
7438
+ * We cannot wait for the cold stream to start after a navigation already happened for example.
7439
+ */
7185
7440
  makeItHot
7186
7441
  );
7187
7442
  const waitForViewportFree$ = state$.pipe(
@@ -7189,6 +7444,24 @@
7189
7444
  rxjs.take(1)
7190
7445
  );
7191
7446
  const navigationAdjustedAfterLayout$ = spine.$.layout$.pipe(
7447
+ /**
7448
+ * @important
7449
+ * Careful with using debounce / throttle here since it can decrease user experience
7450
+ * when layout happens it can means an item before the current one has been unloaded, at current code
7451
+ * we unload and size back each item to the screen so it will have the effect of flicker for user.
7452
+ * Consider this workflow:
7453
+ * - user navigate to page 2
7454
+ * - viewport move to item 2
7455
+ * - page 1 unload and goes back from 2000px to 500px
7456
+ * - layout triggered
7457
+ * - viewport is now on an item far after item 2 because item 1 shrink (PROBLEM)
7458
+ * - sometime after viewport is adjusted back to item 2.
7459
+ *
7460
+ * Two solution to fix this issue:
7461
+ * - maybe later try to implement a different strategy and never shrink back item unless they are loaded
7462
+ * - do not use debounce / throttle and navigate back to the item right on the same tick
7463
+ */
7464
+ // debounceTime(10, animationFrameScheduler),
7192
7465
  switchMap(
7193
7466
  () => waitForViewportFree$.pipe(
7194
7467
  switchMap(() => {
@@ -7258,10 +7531,18 @@
7258
7531
  if (context.isRTL()) {
7259
7532
  return {
7260
7533
  x: leftEnd - position.x - context.getPageSize().width,
7534
+ // y: (topEnd - position.y) - context.getPageSize().height,
7261
7535
  y: Math.max(0, position.y - topStart)
7262
7536
  };
7263
7537
  }
7264
7538
  return {
7539
+ /**
7540
+ * when using spread the item could be on the right and therefore will be negative
7541
+ * @example
7542
+ * 400 (position = viewport), page of 200
7543
+ * 400 - 600 = -200.
7544
+ * However we can assume we are at 0, because we in fact can see the beginning of the item
7545
+ */
7265
7546
  x: Math.max(0, position.x - leftStart),
7266
7547
  y: Math.max(0, position.y - topStart)
7267
7548
  };
@@ -7273,6 +7554,7 @@
7273
7554
  if (context.isRTL()) {
7274
7555
  return {
7275
7556
  x: leftEnd - spineItemPosition.x - context.getPageSize().width,
7557
+ // y: (topEnd - spineItemPosition.y) - context.getPageSize().height,
7276
7558
  y: topStart + spineItemPosition.y
7277
7559
  };
7278
7560
  }
@@ -7613,10 +7895,23 @@
7613
7895
  destroy,
7614
7896
  setSettings: context.setSettings,
7615
7897
  settings$: context.$.settings$,
7898
+ /**
7899
+ * @important
7900
+ * BehaviorSubject
7901
+ */
7616
7902
  pagination$: pagination.$.info$,
7617
7903
  $: {
7618
7904
  state$: stateSubject$.asObservable(),
7905
+ /**
7906
+ * Dispatched when the reader has loaded a book and is displayed a book.
7907
+ * Using navigation API and getting information about current content will
7908
+ * have an effect.
7909
+ * It can typically be used to hide a loading indicator.
7910
+ */
7619
7911
  ready$: readySubject$.asObservable(),
7912
+ /**
7913
+ * Dispatched when a change in selection happens
7914
+ */
7620
7915
  selection$: selectionSubject$.asObservable(),
7621
7916
  viewportState$: viewportNavigator.$.state$,
7622
7917
  layout$: spine.$.layout$,
@@ -7868,6 +8163,10 @@
7868
8163
  };
7869
8164
  return {
7870
8165
  ...reader,
8166
+ // $: {
8167
+ // ...reader.$,
8168
+ // errors$: merge(reader.$.errors$, errorsSubject$.asObservable())
8169
+ // },
7871
8170
  destroy,
7872
8171
  load
7873
8172
  };
@@ -8006,7 +8305,11 @@
8006
8305
  `prose-reader-accessibility`,
8007
8306
  `
8008
8307
  :focus-visible {
8009
- ${``}
8308
+ ${/*
8309
+ Some epubs remove the outline, this is not good practice since it reduce accessibility.
8310
+ We will try to restore it by force.
8311
+ */
8312
+ ``}
8010
8313
  outline: -webkit-focus-ring-color auto 1px;
8011
8314
  }
8012
8315
  `
@@ -8161,23 +8464,27 @@
8161
8464
  container.appendChild(detailsElement);
8162
8465
  return container;
8163
8466
  };
8164
- const createReaderWithEnhancers = loadingEnhancer(
8165
- webkitEnhancer(
8166
- fontsEnhancer(
8167
- linksEnhancer(
8168
- accessibilityEnhancer(
8169
- resourcesEnhancer(
8170
- utilsEnhancer(
8171
- layoutEnhancer(
8172
- zoomEnhancer(
8173
- mediaEnhancer(
8174
- chromeEnhancer(
8175
- navigationEnhancer(
8176
- themeEnhancer(
8177
- hotkeysEnhancer(
8178
- paginationEnhancer(
8179
- progressionEnhancer(
8180
- createReader
8467
+ const createReaderWithEnhancers = (
8468
+ //__
8469
+ loadingEnhancer(
8470
+ webkitEnhancer(
8471
+ fontsEnhancer(
8472
+ linksEnhancer(
8473
+ accessibilityEnhancer(
8474
+ resourcesEnhancer(
8475
+ utilsEnhancer(
8476
+ layoutEnhancer(
8477
+ zoomEnhancer(
8478
+ mediaEnhancer(
8479
+ chromeEnhancer(
8480
+ navigationEnhancer(
8481
+ themeEnhancer(
8482
+ hotkeysEnhancer(
8483
+ paginationEnhancer(
8484
+ progressionEnhancer(
8485
+ // __
8486
+ createReader
8487
+ )
8181
8488
  )
8182
8489
  )
8183
8490
  )