@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.
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
  }),
@@ -2760,28 +2820,34 @@ const paginationEnhancer = (next) => (options) => {
2760
2820
  const buildChapterInfoFromSpineItem = (manifest, item) => {
2761
2821
  var _a;
2762
2822
  const { href } = item;
2763
- return getChapterInfo(href, ((_a = manifest.nav) == null ? void 0 : _a.toc) ?? []);
2823
+ return getChapterInfo(href, ((_a = manifest.nav) == null ? void 0 : _a.toc) ?? [], manifest);
2764
2824
  };
2765
- const getChapterInfo = (href, tocItems) => {
2766
- return tocItems.reduce((acc, tocItem) => {
2767
- const indexOfHash = tocItem.href.indexOf(`#`);
2768
- const tocItemPathWithoutAnchor = indexOfHash > 0 ? tocItem.href.substr(0, indexOfHash) : tocItem.href;
2825
+ const getChapterInfo = (href, tocItem, manifest) => {
2826
+ const spineItemIndex = manifest.spineItems.findIndex((item) => item.href === href);
2827
+ return tocItem.reduce((acc, tocItem2) => {
2828
+ const indexOfHash = tocItem2.href.indexOf(`#`);
2829
+ const tocItemPathWithoutAnchor = indexOfHash > 0 ? tocItem2.href.substr(0, indexOfHash) : tocItem2.href;
2769
2830
  const tocItemHrefWithoutFilename = tocItemPathWithoutAnchor.substring(0, tocItemPathWithoutAnchor.lastIndexOf("/"));
2770
2831
  const hrefWithoutFilename = href.substring(0, href.lastIndexOf("/"));
2771
2832
  const hrefIsChapterHref = href.endsWith(tocItemPathWithoutAnchor);
2772
2833
  const hrefIsWithinChapter = hrefWithoutFilename !== "" && hrefWithoutFilename.endsWith(tocItemHrefWithoutFilename);
2773
- if (hrefIsChapterHref || hrefIsWithinChapter) {
2834
+ const isPossibleTocItemCandidate = hrefIsChapterHref || hrefIsWithinChapter;
2835
+ if (isPossibleTocItemCandidate) {
2836
+ const spineItemIndexOfPossibleCandidate = manifest.spineItems.findIndex((item) => item.href === tocItem2.href);
2837
+ const spineItemIsBeforeThisTocItem = spineItemIndex < spineItemIndexOfPossibleCandidate;
2838
+ if (spineItemIsBeforeThisTocItem)
2839
+ return acc;
2774
2840
  return {
2775
- title: tocItem.title,
2776
- path: tocItem.path
2841
+ title: tocItem2.title,
2842
+ path: tocItem2.path
2777
2843
  };
2778
2844
  }
2779
- const subInfo = getChapterInfo(href, tocItem.contents);
2845
+ const subInfo = getChapterInfo(href, tocItem2.contents, manifest);
2780
2846
  if (subInfo) {
2781
2847
  return {
2782
2848
  subChapter: subInfo,
2783
- title: tocItem.title,
2784
- path: tocItem.path
2849
+ title: tocItem2.title,
2850
+ path: tocItem2.path
2785
2851
  };
2786
2852
  }
2787
2853
  return acc;
@@ -2814,7 +2880,11 @@ const themeEnhancer = (next) => (options) => {
2814
2880
  }
2815
2881
  ${(foundTheme == null ? void 0 : foundTheme.foregroundColor) ? `
2816
2882
  body * {
2817
- ${``}
2883
+ ${/*
2884
+ Ideally, we would like to use !important but it could break publisher specific
2885
+ cases
2886
+ */
2887
+ ``}
2818
2888
  color: ${foundTheme.foregroundColor};
2819
2889
  }
2820
2890
  ` : ``}
@@ -3412,6 +3482,8 @@ const createLoader = ({
3412
3482
  take(1)
3413
3483
  );
3414
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
3415
3487
  withLatestFrom(frameElementSubject$),
3416
3488
  filter(([_, frame]) => !!frame),
3417
3489
  map(([, frame]) => {
@@ -3432,13 +3504,16 @@ const createLoader = ({
3432
3504
  const load$ = loadSubject$.asObservable().pipe(
3433
3505
  withLatestFrom(isLoadedSubject$),
3434
3506
  filter(([_, isLoaded]) => !isLoaded),
3507
+ // let's ignore later load as long as the first one still runs
3435
3508
  exhaustMap(() => {
3436
3509
  return createFrame$().pipe(
3437
3510
  mergeMap((frame) => waitForViewportFree$.pipe(map(() => frame))),
3438
3511
  mergeMap((frame) => {
3439
3512
  parent.appendChild(frame);
3440
3513
  frameElementSubject$.next(frame);
3441
- 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)))) {
3442
3517
  frame == null ? void 0 : frame.setAttribute(`src`, item.href);
3443
3518
  return of(frame);
3444
3519
  } else {
@@ -3494,6 +3569,7 @@ const createLoader = ({
3494
3569
  })
3495
3570
  );
3496
3571
  }),
3572
+ // we stop loading as soon as unload is requested
3497
3573
  takeUntil(unloadSubject$)
3498
3574
  );
3499
3575
  }),
@@ -3620,6 +3696,12 @@ const createFrameItem = ({
3620
3696
  getHtmlFromResource,
3621
3697
  load,
3622
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
+ */
3623
3705
  staticLayout: (size) => {
3624
3706
  const frame = frameElement$.getValue();
3625
3707
  if (frame) {
@@ -3630,6 +3712,8 @@ const createFrameItem = ({
3630
3712
  }
3631
3713
  }
3632
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
3633
3717
  getManipulableFrame,
3634
3718
  getReadingDirection: () => {
3635
3719
  var _a;
@@ -3651,6 +3735,10 @@ const createFrameItem = ({
3651
3735
  loaded$,
3652
3736
  ready$,
3653
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
+ */
3654
3742
  contentLayoutChange$
3655
3743
  }
3656
3744
  };
@@ -3929,6 +4017,8 @@ const createCommonSpineItem = ({
3929
4017
  const rect = containerElement.getBoundingClientRect();
3930
4018
  const normalizedValues = {
3931
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
3932
4022
  width: Math.round(rect.width * 10) / 10,
3933
4023
  height: Math.round(rect.height * 10) / 10
3934
4024
  };
@@ -4008,6 +4098,8 @@ const createCommonSpineItem = ({
4008
4098
  return {
4009
4099
  columnHeight,
4010
4100
  columnWidth,
4101
+ // horizontalMargin,
4102
+ // verticalMargin,
4011
4103
  width
4012
4104
  };
4013
4105
  };
@@ -4325,22 +4417,37 @@ const buildDocumentStyle = ({
4325
4417
  justify-content: ${spreadPosition === `left` ? `flex-end` : spreadPosition === `right` ? `flex-start` : `center`};
4326
4418
  ` : ``}
4327
4419
  }
4328
- ${``}
4420
+ ${/*
4421
+ might be html * but it does mess up things like figure if so.
4422
+ check accessible_epub_3
4423
+ */
4424
+ ``}
4329
4425
  html, body {
4330
4426
  height: 100%;
4331
4427
  width: 100%;
4332
4428
  }
4333
- ${``}
4429
+ ${/*
4430
+ This one is important for preventing 100% img to resize above
4431
+ current width. Especially needed for cbz conversion
4432
+ */
4433
+ ``}
4334
4434
  html, body {
4335
4435
  -max-width: ${columnWidth}px !important;
4336
4436
  }
4337
- ${``}
4437
+ ${/*
4438
+ * @see https://hammerjs.github.io/touch-action/
4439
+ * It needs to be disabled when using free scroll
4440
+ */
4441
+ ``}
4338
4442
  html, body {
4339
4443
  ${enableTouch ? `
4340
4444
  touch-action: none
4341
4445
  ` : ``}
4342
4446
  }
4343
- ${``}
4447
+ ${/*
4448
+ prevent drag of image instead of touch on firefox
4449
+ */
4450
+ ``}
4344
4451
  img {
4345
4452
  user-select: none;
4346
4453
  -webkit-user-drag: none;
@@ -4348,9 +4455,18 @@ const buildDocumentStyle = ({
4348
4455
  -moz-user-drag: none;
4349
4456
  -o-user-drag: none;
4350
4457
  user-drag: none;
4351
- ${``}
4458
+ ${/*
4459
+ prevent weird overflow or margin. Try `block` if `flex` has weird behavior
4460
+ */
4461
+ ``}
4352
4462
  display: flex;
4353
- ${``}
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
+ ``}
4354
4470
  ${!viewportDimensions ? `
4355
4471
  -width: 100%;
4356
4472
  max-width: 100%;
@@ -4481,7 +4597,10 @@ const buildStyleForViewportFrame = () => {
4481
4597
  height: 100%;
4482
4598
  margin: 0;
4483
4599
  }
4484
- ${``}
4600
+ ${/*
4601
+ * @see https://hammerjs.github.io/touch-action/
4602
+ */
4603
+ ``}
4485
4604
  html, body {
4486
4605
  touch-action: none;
4487
4606
  }
@@ -4489,7 +4608,10 @@ const buildStyleForViewportFrame = () => {
4489
4608
  };
4490
4609
  const buildStyleForReflowableImageOnly = ({ isScrollable, enableTouch }) => {
4491
4610
  return `
4492
- ${``}
4611
+ ${/*
4612
+ * @see https://hammerjs.github.io/touch-action/
4613
+ */
4614
+ ``}
4493
4615
  html, body {
4494
4616
  width: 100%;
4495
4617
  margin: 0;
@@ -4504,9 +4626,14 @@ const buildStyleForReflowableImageOnly = ({ isScrollable, enableTouch }) => {
4504
4626
  margin: 0;
4505
4627
  padding: 0;
4506
4628
  box-sizing: border-box;
4507
- ${``}
4629
+ ${// we make sure img spread on entire screen
4630
+ ``}
4508
4631
  width: 100%;
4509
- ${``}
4632
+ ${/**
4633
+ * line break issue
4634
+ * @see https://stackoverflow.com/questions/37869020/image-not-taking-up-the-full-height-of-container
4635
+ */
4636
+ ``}
4510
4637
  display: block;
4511
4638
  }
4512
4639
  ` : ``}
@@ -4521,13 +4648,28 @@ const buildStyleWithMultiColumn = ({
4521
4648
  parsererror {
4522
4649
  display: none !important;
4523
4650
  }
4524
- ${``}
4651
+ ${/*
4652
+ might be html * but it does mess up things like figure if so.
4653
+ check accessible_epub_3
4654
+ */
4655
+ ``}
4525
4656
  html, body {
4526
4657
  margin: 0;
4527
4658
  padding: 0 !important;
4528
4659
  -max-width: ${columnWidth}px !important;
4529
4660
  }
4530
- ${``}
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
+ ``}
4531
4673
  body {
4532
4674
  padding: 0 !important;
4533
4675
  width: ${width}px !important;
@@ -4543,18 +4685,33 @@ const buildStyleWithMultiColumn = ({
4543
4685
  margin: 0;
4544
4686
  }
4545
4687
  body:focus-visible {
4546
- ${``}
4688
+ ${/*
4689
+ we make sure that there are no outline when we focus something inside the iframe
4690
+ */
4691
+ ``}
4547
4692
  outline: none;
4548
4693
  }
4549
- ${``}
4694
+ ${/*
4695
+ * @see https://hammerjs.github.io/touch-action/
4696
+ */
4697
+ ``}
4550
4698
  html, body {
4551
4699
  touch-action: none;
4552
4700
  }
4553
- ${``}
4701
+ ${/*
4702
+ this messes up hard, be careful with this
4703
+ */
4704
+ ``}
4554
4705
  * {
4555
4706
  -max-width: ${columnWidth}px !important;
4556
4707
  }
4557
- ${``}
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
+ ``}
4558
4715
  img, video, audio, object, svg {
4559
4716
  max-width: 100%;
4560
4717
  max-width: ${columnWidth}px !important;
@@ -4573,7 +4730,17 @@ const buildStyleWithMultiColumn = ({
4573
4730
  box-sizing: border-box;
4574
4731
  d-max-width: ${columnWidth}px !important;
4575
4732
  }
4576
- ${``}
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
+ ``}
4577
4744
  table {
4578
4745
  max-width: ${columnWidth}px !important;
4579
4746
  table-layout: fixed;
@@ -4770,7 +4937,11 @@ class CFI {
4770
4937
  this.isRange = false;
4771
4938
  this.opts = Object.assign(
4772
4939
  {
4940
+ // If CFI is a Simple Range, pretend it isn't
4941
+ // by parsing only the start of the range
4773
4942
  flattenRange: false,
4943
+ // Strip temporal, spatial, offset and textLocationAssertion
4944
+ // from places where they don't make sense
4774
4945
  stricter: true
4775
4946
  },
4776
4947
  opts || {}
@@ -4890,6 +5061,7 @@ class CFI {
4890
5061
  return cfi.get();
4891
5062
  }
4892
5063
  }
5064
+ // Takes two CFI paths and compares them
4893
5065
  static comparePath(a, b) {
4894
5066
  const max = Math.max(a.length, b.length);
4895
5067
  let i, cA, cB, diff;
@@ -4906,11 +5078,13 @@ class CFI {
4906
5078
  }
4907
5079
  return 0;
4908
5080
  }
5081
+ // Sort an array of CFI objects
4909
5082
  static sort(a) {
4910
5083
  a.sort((a2, b) => {
4911
5084
  return this.compare(a2, b);
4912
5085
  });
4913
5086
  }
5087
+ // Takes two CFI objects and compares them.
4914
5088
  static compare(a, b) {
4915
5089
  let oA = a.get();
4916
5090
  let oB = b.get();
@@ -4930,6 +5104,7 @@ class CFI {
4930
5104
  return this.comparePath(oA, oB);
4931
5105
  }
4932
5106
  }
5107
+ // Takes two parsed path parts (assuming path is split on '!') and compares them.
4933
5108
  static compareParts(a, b) {
4934
5109
  const max = Math.max(a.length, b.length);
4935
5110
  let i, cA, cB, diff;
@@ -4971,6 +5146,7 @@ class CFI {
4971
5146
  return str;
4972
5147
  }
4973
5148
  }
5149
+ // decode HTML/XML entities and compute length
4974
5150
  trueLength(dom, str) {
4975
5151
  return this.decodeEntities(dom, str).length;
4976
5152
  }
@@ -5236,6 +5412,9 @@ class CFI {
5236
5412
  throw new Error(`Missing child node index in CFI`);
5237
5413
  return { parsed: o, offset: i, newDoc: state === `!` };
5238
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
5239
5418
  getChildNodeByCFIIndex(dom, parentNode, index, offset) {
5240
5419
  const children = parentNode.childNodes;
5241
5420
  if (!children.length)
@@ -5314,6 +5493,7 @@ class CFI {
5314
5493
  }
5315
5494
  return false;
5316
5495
  }
5496
+ // Use a Text Location Assertion to correct and offset
5317
5497
  correctOffset(dom, node, offset, assertion) {
5318
5498
  let curNode = node;
5319
5499
  let matchStr;
@@ -5411,6 +5591,15 @@ class CFI {
5411
5591
  }
5412
5592
  return o;
5413
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
5414
5603
  resolveURI(index, dom, opts) {
5415
5604
  opts = opts || {};
5416
5605
  if (index < 0 || index > this.parts.length - 2) {
@@ -5422,7 +5611,8 @@ class CFI {
5422
5611
  const o = this.resolveNode(index, subparts, dom, opts);
5423
5612
  let node = o.node;
5424
5613
  const tagName = node.tagName.toLowerCase();
5425
- if (tagName === `itemref` && node.parentNode.tagName.toLowerCase() === `spine`) {
5614
+ if (tagName === `itemref` && // @ts-ignore
5615
+ node.parentNode.tagName.toLowerCase() === `spine`) {
5426
5616
  const idref = node.getAttribute(`idref`);
5427
5617
  if (!idref)
5428
5618
  throw new Error(`Referenced node had not 'idref' attribute`);
@@ -5469,6 +5659,9 @@ class CFI {
5469
5659
  delete o.offset;
5470
5660
  return { ...lastPart, ...o };
5471
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
5472
5665
  resolveLast(dom, opts) {
5473
5666
  opts = Object.assign(
5474
5667
  {
@@ -5726,7 +5919,22 @@ const createSpine = ({
5726
5919
  spineItem: beginSpineItem,
5727
5920
  spineItemIndex: beginItemIndex,
5728
5921
  pageIndex: beginPageIndex,
5729
- 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
+ ),
5730
5938
  options: {
5731
5939
  isAtEndOfChapter: false
5732
5940
  }
@@ -5735,7 +5943,10 @@ const createSpine = ({
5735
5943
  spineItem: endSpineItem,
5736
5944
  spineItemIndex: endItemIndex,
5737
5945
  pageIndex: endPageIndex,
5738
- 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
+ ),
5739
5950
  options: {
5740
5951
  isAtEndOfChapter: false
5741
5952
  }
@@ -5765,7 +5976,16 @@ const createSpine = ({
5765
5976
  takeUntil(context.$.destroy$)
5766
5977
  ).subscribe();
5767
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
+ */
5768
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
+ */
5769
5989
  navigationAdjusted$
5770
5990
  ).pipe(
5771
5991
  switchMap(() => {
@@ -5964,7 +6184,10 @@ const createSpineItemManager = ({ context }) => {
5964
6184
  const isPrePaginated = ((_a = context.getManifest()) == null ? void 0 : _a.renditionLayout) === `pre-paginated`;
5965
6185
  const isUsingFreeScroll = context.getSettings().computedPageTurnMode === `scrollable`;
5966
6186
  orderedSpineItemsSubject$.value.forEach((orderedSpineItem, index) => {
5967
- 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
+ );
5968
6191
  const isAfterTailWithPreload = index > rightIndex + numberOfAdjacentSpineItemToPreLoad;
5969
6192
  if (!isBeforeFocusedWithPreload && !isAfterTailWithPreload) {
5970
6193
  orderedSpineItem.loadContent();
@@ -6170,7 +6393,8 @@ const createLocationResolver$1 = ({ context }) => {
6170
6393
  var _a, _b, _c;
6171
6394
  const pageSize = context.getPageSize();
6172
6395
  const frame = (_b = (_a = spineItem.spineItemFrame) == null ? void 0 : _a.getManipulableFrame()) == null ? void 0 : _b.frame;
6173
- 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) {
6174
6398
  const { x: left, y: top } = getSpineItemPositionFromPageIndex(pageIndex, spineItem);
6175
6399
  const viewport = {
6176
6400
  left,
@@ -6740,8 +6964,14 @@ const createManualViewportNavigator = ({
6740
6964
  chapterPageNavigation$,
6741
6965
  leftPageNavigation$,
6742
6966
  rightPageNavigation$,
6967
+ // for some reason after too much item ts complains
6743
6968
  merge(cfiNavigation$, pageNavigation$)
6744
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
+ */
6745
6975
  withLatestFrom(currentNavigationSubject$),
6746
6976
  filter(([navigation, currentNavigation]) => navigator2.areNavigationDifferent(navigation, currentNavigation)),
6747
6977
  map(([navigation]) => navigation)
@@ -6916,6 +7146,9 @@ const createViewportNavigator = ({
6916
7146
  }
6917
7147
  const { x, y } = element.getBoundingClientRect();
6918
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 ~~
6919
7152
  x: ~~(Math.abs(x) * 10) / 10,
6920
7153
  y: ~~(Math.abs(y) * 10) / 10
6921
7154
  };
@@ -7126,6 +7359,14 @@ const createViewportNavigator = ({
7126
7359
  const animationDuration = currentEvent.animation === `snap` ? context.getSettings().computedSnapAnimationDuration : context.getSettings().computedPageTurnAnimationDuration;
7127
7360
  const pageTurnAnimation = currentEvent.animation === `snap` ? `slide` : context.getSettings().computedPageTurnAnimation;
7128
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
+ */
7129
7370
  currentEvent.shouldAnimate ? delay(1, animationFrameScheduler) : identity$1,
7130
7371
  tap((data) => {
7131
7372
  const noAdjustmentNeeded = false;
@@ -7142,6 +7383,14 @@ const createViewportNavigator = ({
7142
7383
  element.style.setProperty(`opacity`, `1`);
7143
7384
  }
7144
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
+ */
7145
7394
  withLatestFrom(hooks$),
7146
7395
  tap(([data, hooks]) => {
7147
7396
  if (pageTurnAnimation !== `fade`) {
@@ -7180,6 +7429,12 @@ const createViewportNavigator = ({
7180
7429
  map((states) => states.every((state) => state === `end`) ? `free` : `busy`),
7181
7430
  distinctUntilChanged(),
7182
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
+ */
7183
7438
  makeItHot
7184
7439
  );
7185
7440
  const waitForViewportFree$ = state$.pipe(
@@ -7187,6 +7442,24 @@ const createViewportNavigator = ({
7187
7442
  take$1(1)
7188
7443
  );
7189
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),
7190
7463
  switchMap(
7191
7464
  () => waitForViewportFree$.pipe(
7192
7465
  switchMap(() => {
@@ -7256,10 +7529,18 @@ const createLocationResolver = ({
7256
7529
  if (context.isRTL()) {
7257
7530
  return {
7258
7531
  x: leftEnd - position.x - context.getPageSize().width,
7532
+ // y: (topEnd - position.y) - context.getPageSize().height,
7259
7533
  y: Math.max(0, position.y - topStart)
7260
7534
  };
7261
7535
  }
7262
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
+ */
7263
7544
  x: Math.max(0, position.x - leftStart),
7264
7545
  y: Math.max(0, position.y - topStart)
7265
7546
  };
@@ -7271,6 +7552,7 @@ const createLocationResolver = ({
7271
7552
  if (context.isRTL()) {
7272
7553
  return {
7273
7554
  x: leftEnd - spineItemPosition.x - context.getPageSize().width,
7555
+ // y: (topEnd - spineItemPosition.y) - context.getPageSize().height,
7274
7556
  y: topStart + spineItemPosition.y
7275
7557
  };
7276
7558
  }
@@ -7611,10 +7893,23 @@ const createReader = ({ containerElement, hooks: initialHooks, ...settings }) =>
7611
7893
  destroy,
7612
7894
  setSettings: context.setSettings,
7613
7895
  settings$: context.$.settings$,
7896
+ /**
7897
+ * @important
7898
+ * BehaviorSubject
7899
+ */
7614
7900
  pagination$: pagination.$.info$,
7615
7901
  $: {
7616
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
+ */
7617
7909
  ready$: readySubject$.asObservable(),
7910
+ /**
7911
+ * Dispatched when a change in selection happens
7912
+ */
7618
7913
  selection$: selectionSubject$.asObservable(),
7619
7914
  viewportState$: viewportNavigator.$.state$,
7620
7915
  layout$: spine.$.layout$,
@@ -7866,6 +8161,10 @@ const resourcesEnhancer = (next) => (options) => {
7866
8161
  };
7867
8162
  return {
7868
8163
  ...reader,
8164
+ // $: {
8165
+ // ...reader.$,
8166
+ // errors$: merge(reader.$.errors$, errorsSubject$.asObservable())
8167
+ // },
7869
8168
  destroy,
7870
8169
  load
7871
8170
  };
@@ -8004,7 +8303,11 @@ const accessibilityEnhancer = (next) => (options) => {
8004
8303
  `prose-reader-accessibility`,
8005
8304
  `
8006
8305
  :focus-visible {
8007
- ${``}
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
+ ``}
8008
8311
  outline: -webkit-focus-ring-color auto 1px;
8009
8312
  }
8010
8313
  `
@@ -8159,23 +8462,27 @@ const defaultLoadingElementCreate = ({ container, item }) => {
8159
8462
  container.appendChild(detailsElement);
8160
8463
  return container;
8161
8464
  };
8162
- const createReaderWithEnhancers = loadingEnhancer(
8163
- webkitEnhancer(
8164
- fontsEnhancer(
8165
- linksEnhancer(
8166
- accessibilityEnhancer(
8167
- resourcesEnhancer(
8168
- utilsEnhancer(
8169
- layoutEnhancer(
8170
- zoomEnhancer(
8171
- mediaEnhancer(
8172
- chromeEnhancer(
8173
- navigationEnhancer(
8174
- themeEnhancer(
8175
- hotkeysEnhancer(
8176
- paginationEnhancer(
8177
- progressionEnhancer(
8178
- 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
+ )
8179
8486
  )
8180
8487
  )
8181
8488
  )