@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 +344 -43
- package/dist/prose.js.map +1 -1
- package/dist/prose.umd.cjs +344 -43
- package/dist/prose.umd.cjs.map +1 -1
- package/package.json +4 -11
package/dist/prose.umd.cjs
CHANGED
|
@@ -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
|
}),
|
|
@@ -2822,7 +2882,11 @@
|
|
|
2822
2882
|
}
|
|
2823
2883
|
${(foundTheme == null ? void 0 : foundTheme.foregroundColor) ? `
|
|
2824
2884
|
body * {
|
|
2825
|
-
${
|
|
2885
|
+
${/*
|
|
2886
|
+
Ideally, we would like to use !important but it could break publisher specific
|
|
2887
|
+
cases
|
|
2888
|
+
*/
|
|
2889
|
+
``}
|
|
2826
2890
|
color: ${foundTheme.foregroundColor};
|
|
2827
2891
|
}
|
|
2828
2892
|
` : ``}
|
|
@@ -3420,6 +3484,8 @@
|
|
|
3420
3484
|
take(1)
|
|
3421
3485
|
);
|
|
3422
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
|
|
3423
3489
|
withLatestFrom(frameElementSubject$),
|
|
3424
3490
|
filter(([_, frame]) => !!frame),
|
|
3425
3491
|
map(([, frame]) => {
|
|
@@ -3440,13 +3506,16 @@
|
|
|
3440
3506
|
const load$ = loadSubject$.asObservable().pipe(
|
|
3441
3507
|
withLatestFrom(isLoadedSubject$),
|
|
3442
3508
|
filter(([_, isLoaded]) => !isLoaded),
|
|
3509
|
+
// let's ignore later load as long as the first one still runs
|
|
3443
3510
|
exhaustMap(() => {
|
|
3444
3511
|
return createFrame$().pipe(
|
|
3445
3512
|
mergeMap((frame) => waitForViewportFree$.pipe(map(() => frame))),
|
|
3446
3513
|
mergeMap((frame) => {
|
|
3447
3514
|
parent.appendChild(frame);
|
|
3448
3515
|
frameElementSubject$.next(frame);
|
|
3449
|
-
if (!fetchResource && item.href.startsWith(window.location.origin) &&
|
|
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)))) {
|
|
3450
3519
|
frame == null ? void 0 : frame.setAttribute(`src`, item.href);
|
|
3451
3520
|
return rxjs.of(frame);
|
|
3452
3521
|
} else {
|
|
@@ -3502,6 +3571,7 @@
|
|
|
3502
3571
|
})
|
|
3503
3572
|
);
|
|
3504
3573
|
}),
|
|
3574
|
+
// we stop loading as soon as unload is requested
|
|
3505
3575
|
takeUntil(unloadSubject$)
|
|
3506
3576
|
);
|
|
3507
3577
|
}),
|
|
@@ -3628,6 +3698,12 @@
|
|
|
3628
3698
|
getHtmlFromResource,
|
|
3629
3699
|
load,
|
|
3630
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
|
+
*/
|
|
3631
3707
|
staticLayout: (size) => {
|
|
3632
3708
|
const frame = frameElement$.getValue();
|
|
3633
3709
|
if (frame) {
|
|
@@ -3638,6 +3714,8 @@
|
|
|
3638
3714
|
}
|
|
3639
3715
|
}
|
|
3640
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
|
|
3641
3719
|
getManipulableFrame,
|
|
3642
3720
|
getReadingDirection: () => {
|
|
3643
3721
|
var _a;
|
|
@@ -3659,6 +3737,10 @@
|
|
|
3659
3737
|
loaded$,
|
|
3660
3738
|
ready$,
|
|
3661
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
|
+
*/
|
|
3662
3744
|
contentLayoutChange$
|
|
3663
3745
|
}
|
|
3664
3746
|
};
|
|
@@ -3937,6 +4019,8 @@
|
|
|
3937
4019
|
const rect = containerElement.getBoundingClientRect();
|
|
3938
4020
|
const normalizedValues = {
|
|
3939
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
|
|
3940
4024
|
width: Math.round(rect.width * 10) / 10,
|
|
3941
4025
|
height: Math.round(rect.height * 10) / 10
|
|
3942
4026
|
};
|
|
@@ -4016,6 +4100,8 @@
|
|
|
4016
4100
|
return {
|
|
4017
4101
|
columnHeight,
|
|
4018
4102
|
columnWidth,
|
|
4103
|
+
// horizontalMargin,
|
|
4104
|
+
// verticalMargin,
|
|
4019
4105
|
width
|
|
4020
4106
|
};
|
|
4021
4107
|
};
|
|
@@ -4333,22 +4419,37 @@
|
|
|
4333
4419
|
justify-content: ${spreadPosition === `left` ? `flex-end` : spreadPosition === `right` ? `flex-start` : `center`};
|
|
4334
4420
|
` : ``}
|
|
4335
4421
|
}
|
|
4336
|
-
${
|
|
4422
|
+
${/*
|
|
4423
|
+
might be html * but it does mess up things like figure if so.
|
|
4424
|
+
check accessible_epub_3
|
|
4425
|
+
*/
|
|
4426
|
+
``}
|
|
4337
4427
|
html, body {
|
|
4338
4428
|
height: 100%;
|
|
4339
4429
|
width: 100%;
|
|
4340
4430
|
}
|
|
4341
|
-
${
|
|
4431
|
+
${/*
|
|
4432
|
+
This one is important for preventing 100% img to resize above
|
|
4433
|
+
current width. Especially needed for cbz conversion
|
|
4434
|
+
*/
|
|
4435
|
+
``}
|
|
4342
4436
|
html, body {
|
|
4343
4437
|
-max-width: ${columnWidth}px !important;
|
|
4344
4438
|
}
|
|
4345
|
-
${
|
|
4439
|
+
${/*
|
|
4440
|
+
* @see https://hammerjs.github.io/touch-action/
|
|
4441
|
+
* It needs to be disabled when using free scroll
|
|
4442
|
+
*/
|
|
4443
|
+
``}
|
|
4346
4444
|
html, body {
|
|
4347
4445
|
${enableTouch ? `
|
|
4348
4446
|
touch-action: none
|
|
4349
4447
|
` : ``}
|
|
4350
4448
|
}
|
|
4351
|
-
${
|
|
4449
|
+
${/*
|
|
4450
|
+
prevent drag of image instead of touch on firefox
|
|
4451
|
+
*/
|
|
4452
|
+
``}
|
|
4352
4453
|
img {
|
|
4353
4454
|
user-select: none;
|
|
4354
4455
|
-webkit-user-drag: none;
|
|
@@ -4356,9 +4457,18 @@
|
|
|
4356
4457
|
-moz-user-drag: none;
|
|
4357
4458
|
-o-user-drag: none;
|
|
4358
4459
|
user-drag: none;
|
|
4359
|
-
${
|
|
4460
|
+
${/*
|
|
4461
|
+
prevent weird overflow or margin. Try `block` if `flex` has weird behavior
|
|
4462
|
+
*/
|
|
4463
|
+
``}
|
|
4360
4464
|
display: flex;
|
|
4361
|
-
${
|
|
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
|
+
``}
|
|
4362
4472
|
${!viewportDimensions ? `
|
|
4363
4473
|
-width: 100%;
|
|
4364
4474
|
max-width: 100%;
|
|
@@ -4489,7 +4599,10 @@
|
|
|
4489
4599
|
height: 100%;
|
|
4490
4600
|
margin: 0;
|
|
4491
4601
|
}
|
|
4492
|
-
${
|
|
4602
|
+
${/*
|
|
4603
|
+
* @see https://hammerjs.github.io/touch-action/
|
|
4604
|
+
*/
|
|
4605
|
+
``}
|
|
4493
4606
|
html, body {
|
|
4494
4607
|
touch-action: none;
|
|
4495
4608
|
}
|
|
@@ -4497,7 +4610,10 @@
|
|
|
4497
4610
|
};
|
|
4498
4611
|
const buildStyleForReflowableImageOnly = ({ isScrollable, enableTouch }) => {
|
|
4499
4612
|
return `
|
|
4500
|
-
${
|
|
4613
|
+
${/*
|
|
4614
|
+
* @see https://hammerjs.github.io/touch-action/
|
|
4615
|
+
*/
|
|
4616
|
+
``}
|
|
4501
4617
|
html, body {
|
|
4502
4618
|
width: 100%;
|
|
4503
4619
|
margin: 0;
|
|
@@ -4512,9 +4628,14 @@
|
|
|
4512
4628
|
margin: 0;
|
|
4513
4629
|
padding: 0;
|
|
4514
4630
|
box-sizing: border-box;
|
|
4515
|
-
${
|
|
4631
|
+
${// we make sure img spread on entire screen
|
|
4632
|
+
``}
|
|
4516
4633
|
width: 100%;
|
|
4517
|
-
${
|
|
4634
|
+
${/**
|
|
4635
|
+
* line break issue
|
|
4636
|
+
* @see https://stackoverflow.com/questions/37869020/image-not-taking-up-the-full-height-of-container
|
|
4637
|
+
*/
|
|
4638
|
+
``}
|
|
4518
4639
|
display: block;
|
|
4519
4640
|
}
|
|
4520
4641
|
` : ``}
|
|
@@ -4529,13 +4650,28 @@
|
|
|
4529
4650
|
parsererror {
|
|
4530
4651
|
display: none !important;
|
|
4531
4652
|
}
|
|
4532
|
-
${
|
|
4653
|
+
${/*
|
|
4654
|
+
might be html * but it does mess up things like figure if so.
|
|
4655
|
+
check accessible_epub_3
|
|
4656
|
+
*/
|
|
4657
|
+
``}
|
|
4533
4658
|
html, body {
|
|
4534
4659
|
margin: 0;
|
|
4535
4660
|
padding: 0 !important;
|
|
4536
4661
|
-max-width: ${columnWidth}px !important;
|
|
4537
4662
|
}
|
|
4538
|
-
${
|
|
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
|
+
``}
|
|
4539
4675
|
body {
|
|
4540
4676
|
padding: 0 !important;
|
|
4541
4677
|
width: ${width}px !important;
|
|
@@ -4551,18 +4687,33 @@
|
|
|
4551
4687
|
margin: 0;
|
|
4552
4688
|
}
|
|
4553
4689
|
body:focus-visible {
|
|
4554
|
-
${
|
|
4690
|
+
${/*
|
|
4691
|
+
we make sure that there are no outline when we focus something inside the iframe
|
|
4692
|
+
*/
|
|
4693
|
+
``}
|
|
4555
4694
|
outline: none;
|
|
4556
4695
|
}
|
|
4557
|
-
${
|
|
4696
|
+
${/*
|
|
4697
|
+
* @see https://hammerjs.github.io/touch-action/
|
|
4698
|
+
*/
|
|
4699
|
+
``}
|
|
4558
4700
|
html, body {
|
|
4559
4701
|
touch-action: none;
|
|
4560
4702
|
}
|
|
4561
|
-
${
|
|
4703
|
+
${/*
|
|
4704
|
+
this messes up hard, be careful with this
|
|
4705
|
+
*/
|
|
4706
|
+
``}
|
|
4562
4707
|
* {
|
|
4563
4708
|
-max-width: ${columnWidth}px !important;
|
|
4564
4709
|
}
|
|
4565
|
-
${
|
|
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
|
+
``}
|
|
4566
4717
|
img, video, audio, object, svg {
|
|
4567
4718
|
max-width: 100%;
|
|
4568
4719
|
max-width: ${columnWidth}px !important;
|
|
@@ -4581,7 +4732,17 @@
|
|
|
4581
4732
|
box-sizing: border-box;
|
|
4582
4733
|
d-max-width: ${columnWidth}px !important;
|
|
4583
4734
|
}
|
|
4584
|
-
${
|
|
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
|
+
``}
|
|
4585
4746
|
table {
|
|
4586
4747
|
max-width: ${columnWidth}px !important;
|
|
4587
4748
|
table-layout: fixed;
|
|
@@ -4778,7 +4939,11 @@
|
|
|
4778
4939
|
this.isRange = false;
|
|
4779
4940
|
this.opts = Object.assign(
|
|
4780
4941
|
{
|
|
4942
|
+
// If CFI is a Simple Range, pretend it isn't
|
|
4943
|
+
// by parsing only the start of the range
|
|
4781
4944
|
flattenRange: false,
|
|
4945
|
+
// Strip temporal, spatial, offset and textLocationAssertion
|
|
4946
|
+
// from places where they don't make sense
|
|
4782
4947
|
stricter: true
|
|
4783
4948
|
},
|
|
4784
4949
|
opts || {}
|
|
@@ -4898,6 +5063,7 @@
|
|
|
4898
5063
|
return cfi.get();
|
|
4899
5064
|
}
|
|
4900
5065
|
}
|
|
5066
|
+
// Takes two CFI paths and compares them
|
|
4901
5067
|
static comparePath(a, b) {
|
|
4902
5068
|
const max = Math.max(a.length, b.length);
|
|
4903
5069
|
let i, cA, cB, diff;
|
|
@@ -4914,11 +5080,13 @@
|
|
|
4914
5080
|
}
|
|
4915
5081
|
return 0;
|
|
4916
5082
|
}
|
|
5083
|
+
// Sort an array of CFI objects
|
|
4917
5084
|
static sort(a) {
|
|
4918
5085
|
a.sort((a2, b) => {
|
|
4919
5086
|
return this.compare(a2, b);
|
|
4920
5087
|
});
|
|
4921
5088
|
}
|
|
5089
|
+
// Takes two CFI objects and compares them.
|
|
4922
5090
|
static compare(a, b) {
|
|
4923
5091
|
let oA = a.get();
|
|
4924
5092
|
let oB = b.get();
|
|
@@ -4938,6 +5106,7 @@
|
|
|
4938
5106
|
return this.comparePath(oA, oB);
|
|
4939
5107
|
}
|
|
4940
5108
|
}
|
|
5109
|
+
// Takes two parsed path parts (assuming path is split on '!') and compares them.
|
|
4941
5110
|
static compareParts(a, b) {
|
|
4942
5111
|
const max = Math.max(a.length, b.length);
|
|
4943
5112
|
let i, cA, cB, diff;
|
|
@@ -4979,6 +5148,7 @@
|
|
|
4979
5148
|
return str;
|
|
4980
5149
|
}
|
|
4981
5150
|
}
|
|
5151
|
+
// decode HTML/XML entities and compute length
|
|
4982
5152
|
trueLength(dom, str) {
|
|
4983
5153
|
return this.decodeEntities(dom, str).length;
|
|
4984
5154
|
}
|
|
@@ -5244,6 +5414,9 @@
|
|
|
5244
5414
|
throw new Error(`Missing child node index in CFI`);
|
|
5245
5415
|
return { parsed: o, offset: i, newDoc: state === `!` };
|
|
5246
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
|
|
5247
5420
|
getChildNodeByCFIIndex(dom, parentNode, index, offset) {
|
|
5248
5421
|
const children = parentNode.childNodes;
|
|
5249
5422
|
if (!children.length)
|
|
@@ -5322,6 +5495,7 @@
|
|
|
5322
5495
|
}
|
|
5323
5496
|
return false;
|
|
5324
5497
|
}
|
|
5498
|
+
// Use a Text Location Assertion to correct and offset
|
|
5325
5499
|
correctOffset(dom, node, offset, assertion) {
|
|
5326
5500
|
let curNode = node;
|
|
5327
5501
|
let matchStr;
|
|
@@ -5419,6 +5593,15 @@
|
|
|
5419
5593
|
}
|
|
5420
5594
|
return o;
|
|
5421
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
|
|
5422
5605
|
resolveURI(index, dom, opts) {
|
|
5423
5606
|
opts = opts || {};
|
|
5424
5607
|
if (index < 0 || index > this.parts.length - 2) {
|
|
@@ -5430,7 +5613,8 @@
|
|
|
5430
5613
|
const o = this.resolveNode(index, subparts, dom, opts);
|
|
5431
5614
|
let node = o.node;
|
|
5432
5615
|
const tagName = node.tagName.toLowerCase();
|
|
5433
|
-
if (tagName === `itemref` &&
|
|
5616
|
+
if (tagName === `itemref` && // @ts-ignore
|
|
5617
|
+
node.parentNode.tagName.toLowerCase() === `spine`) {
|
|
5434
5618
|
const idref = node.getAttribute(`idref`);
|
|
5435
5619
|
if (!idref)
|
|
5436
5620
|
throw new Error(`Referenced node had not 'idref' attribute`);
|
|
@@ -5477,6 +5661,9 @@
|
|
|
5477
5661
|
delete o.offset;
|
|
5478
5662
|
return { ...lastPart, ...o };
|
|
5479
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
|
|
5480
5667
|
resolveLast(dom, opts) {
|
|
5481
5668
|
opts = Object.assign(
|
|
5482
5669
|
{
|
|
@@ -5734,7 +5921,22 @@
|
|
|
5734
5921
|
spineItem: beginSpineItem,
|
|
5735
5922
|
spineItemIndex: beginItemIndex,
|
|
5736
5923
|
pageIndex: beginPageIndex,
|
|
5737
|
-
|
|
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
|
+
),
|
|
5738
5940
|
options: {
|
|
5739
5941
|
isAtEndOfChapter: false
|
|
5740
5942
|
}
|
|
@@ -5743,7 +5945,10 @@
|
|
|
5743
5945
|
spineItem: endSpineItem,
|
|
5744
5946
|
spineItemIndex: endItemIndex,
|
|
5745
5947
|
pageIndex: endPageIndex,
|
|
5746
|
-
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) :
|
|
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
|
+
),
|
|
5747
5952
|
options: {
|
|
5748
5953
|
isAtEndOfChapter: false
|
|
5749
5954
|
}
|
|
@@ -5773,7 +5978,16 @@
|
|
|
5773
5978
|
takeUntil(context.$.destroy$)
|
|
5774
5979
|
).subscribe();
|
|
5775
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
|
+
*/
|
|
5776
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
|
+
*/
|
|
5777
5991
|
navigationAdjusted$
|
|
5778
5992
|
).pipe(
|
|
5779
5993
|
switchMap(() => {
|
|
@@ -5972,7 +6186,10 @@
|
|
|
5972
6186
|
const isPrePaginated = ((_a = context.getManifest()) == null ? void 0 : _a.renditionLayout) === `pre-paginated`;
|
|
5973
6187
|
const isUsingFreeScroll = context.getSettings().computedPageTurnMode === `scrollable`;
|
|
5974
6188
|
orderedSpineItemsSubject$.value.forEach((orderedSpineItem, index) => {
|
|
5975
|
-
const isBeforeFocusedWithPreload =
|
|
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
|
+
);
|
|
5976
6193
|
const isAfterTailWithPreload = index > rightIndex + numberOfAdjacentSpineItemToPreLoad;
|
|
5977
6194
|
if (!isBeforeFocusedWithPreload && !isAfterTailWithPreload) {
|
|
5978
6195
|
orderedSpineItem.loadContent();
|
|
@@ -6178,7 +6395,8 @@
|
|
|
6178
6395
|
var _a, _b, _c;
|
|
6179
6396
|
const pageSize = context.getPageSize();
|
|
6180
6397
|
const frame = (_b = (_a = spineItem.spineItemFrame) == null ? void 0 : _a.getManipulableFrame()) == null ? void 0 : _b.frame;
|
|
6181
|
-
if (((_c = frame == null ? void 0 : frame.contentWindow) == null ? void 0 : _c.document) &&
|
|
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) {
|
|
6182
6400
|
const { x: left, y: top } = getSpineItemPositionFromPageIndex(pageIndex, spineItem);
|
|
6183
6401
|
const viewport = {
|
|
6184
6402
|
left,
|
|
@@ -6748,8 +6966,14 @@
|
|
|
6748
6966
|
chapterPageNavigation$,
|
|
6749
6967
|
leftPageNavigation$,
|
|
6750
6968
|
rightPageNavigation$,
|
|
6969
|
+
// for some reason after too much item ts complains
|
|
6751
6970
|
rxjs.merge(cfiNavigation$, pageNavigation$)
|
|
6752
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
|
+
*/
|
|
6753
6977
|
withLatestFrom(currentNavigationSubject$),
|
|
6754
6978
|
filter(([navigation, currentNavigation]) => navigator2.areNavigationDifferent(navigation, currentNavigation)),
|
|
6755
6979
|
map(([navigation]) => navigation)
|
|
@@ -6924,6 +7148,9 @@
|
|
|
6924
7148
|
}
|
|
6925
7149
|
const { x, y } = element.getBoundingClientRect();
|
|
6926
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 ~~
|
|
6927
7154
|
x: ~~(Math.abs(x) * 10) / 10,
|
|
6928
7155
|
y: ~~(Math.abs(y) * 10) / 10
|
|
6929
7156
|
};
|
|
@@ -7134,6 +7361,14 @@
|
|
|
7134
7361
|
const animationDuration = currentEvent.animation === `snap` ? context.getSettings().computedSnapAnimationDuration : context.getSettings().computedPageTurnAnimationDuration;
|
|
7135
7362
|
const pageTurnAnimation = currentEvent.animation === `snap` ? `slide` : context.getSettings().computedPageTurnAnimation;
|
|
7136
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
|
+
*/
|
|
7137
7372
|
currentEvent.shouldAnimate ? delay(1, rxjs.animationFrameScheduler) : rxjs.identity,
|
|
7138
7373
|
tap((data) => {
|
|
7139
7374
|
const noAdjustmentNeeded = false;
|
|
@@ -7150,6 +7385,14 @@
|
|
|
7150
7385
|
element.style.setProperty(`opacity`, `1`);
|
|
7151
7386
|
}
|
|
7152
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
|
+
*/
|
|
7153
7396
|
withLatestFrom(hooks$),
|
|
7154
7397
|
tap(([data, hooks]) => {
|
|
7155
7398
|
if (pageTurnAnimation !== `fade`) {
|
|
@@ -7188,6 +7431,12 @@
|
|
|
7188
7431
|
map((states) => states.every((state) => state === `end`) ? `free` : `busy`),
|
|
7189
7432
|
distinctUntilChanged(),
|
|
7190
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
|
+
*/
|
|
7191
7440
|
makeItHot
|
|
7192
7441
|
);
|
|
7193
7442
|
const waitForViewportFree$ = state$.pipe(
|
|
@@ -7195,6 +7444,24 @@
|
|
|
7195
7444
|
rxjs.take(1)
|
|
7196
7445
|
);
|
|
7197
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),
|
|
7198
7465
|
switchMap(
|
|
7199
7466
|
() => waitForViewportFree$.pipe(
|
|
7200
7467
|
switchMap(() => {
|
|
@@ -7264,10 +7531,18 @@
|
|
|
7264
7531
|
if (context.isRTL()) {
|
|
7265
7532
|
return {
|
|
7266
7533
|
x: leftEnd - position.x - context.getPageSize().width,
|
|
7534
|
+
// y: (topEnd - position.y) - context.getPageSize().height,
|
|
7267
7535
|
y: Math.max(0, position.y - topStart)
|
|
7268
7536
|
};
|
|
7269
7537
|
}
|
|
7270
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
|
+
*/
|
|
7271
7546
|
x: Math.max(0, position.x - leftStart),
|
|
7272
7547
|
y: Math.max(0, position.y - topStart)
|
|
7273
7548
|
};
|
|
@@ -7279,6 +7554,7 @@
|
|
|
7279
7554
|
if (context.isRTL()) {
|
|
7280
7555
|
return {
|
|
7281
7556
|
x: leftEnd - spineItemPosition.x - context.getPageSize().width,
|
|
7557
|
+
// y: (topEnd - spineItemPosition.y) - context.getPageSize().height,
|
|
7282
7558
|
y: topStart + spineItemPosition.y
|
|
7283
7559
|
};
|
|
7284
7560
|
}
|
|
@@ -7619,10 +7895,23 @@
|
|
|
7619
7895
|
destroy,
|
|
7620
7896
|
setSettings: context.setSettings,
|
|
7621
7897
|
settings$: context.$.settings$,
|
|
7898
|
+
/**
|
|
7899
|
+
* @important
|
|
7900
|
+
* BehaviorSubject
|
|
7901
|
+
*/
|
|
7622
7902
|
pagination$: pagination.$.info$,
|
|
7623
7903
|
$: {
|
|
7624
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
|
+
*/
|
|
7625
7911
|
ready$: readySubject$.asObservable(),
|
|
7912
|
+
/**
|
|
7913
|
+
* Dispatched when a change in selection happens
|
|
7914
|
+
*/
|
|
7626
7915
|
selection$: selectionSubject$.asObservable(),
|
|
7627
7916
|
viewportState$: viewportNavigator.$.state$,
|
|
7628
7917
|
layout$: spine.$.layout$,
|
|
@@ -7874,6 +8163,10 @@
|
|
|
7874
8163
|
};
|
|
7875
8164
|
return {
|
|
7876
8165
|
...reader,
|
|
8166
|
+
// $: {
|
|
8167
|
+
// ...reader.$,
|
|
8168
|
+
// errors$: merge(reader.$.errors$, errorsSubject$.asObservable())
|
|
8169
|
+
// },
|
|
7877
8170
|
destroy,
|
|
7878
8171
|
load
|
|
7879
8172
|
};
|
|
@@ -8012,7 +8305,11 @@
|
|
|
8012
8305
|
`prose-reader-accessibility`,
|
|
8013
8306
|
`
|
|
8014
8307
|
:focus-visible {
|
|
8015
|
-
${
|
|
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
|
+
``}
|
|
8016
8313
|
outline: -webkit-focus-ring-color auto 1px;
|
|
8017
8314
|
}
|
|
8018
8315
|
`
|
|
@@ -8167,23 +8464,27 @@
|
|
|
8167
8464
|
container.appendChild(detailsElement);
|
|
8168
8465
|
return container;
|
|
8169
8466
|
};
|
|
8170
|
-
const createReaderWithEnhancers =
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
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
|
+
)
|
|
8187
8488
|
)
|
|
8188
8489
|
)
|
|
8189
8490
|
)
|