├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── _lint.yml │ ├── _test.yml │ ├── _ts.yml │ ├── check.yml │ └── check_publish_lib.yml ├── .gitignore ├── .husky └── pre-commit ├── .vscode └── settings.json ├── LICENCE ├── README.md ├── apps ├── demo │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ ├── epubs │ │ │ ├── Accessibility-Tests-Mathematics.epub │ │ │ ├── accessible_epub_3.epub │ │ │ ├── cc-shared-culture.epub │ │ │ ├── haruko-comic.zip │ │ │ ├── haruko-html-jpeg.epub │ │ │ ├── moby-dick_txt.txt │ │ │ ├── mymedia_lite.epub │ │ │ ├── regime-anticancer-arabic.epub │ │ │ ├── rendition-flow-webtoon-one-page.epub │ │ │ ├── rendition-flow-webtoon.epub │ │ │ ├── sample-3.pdf │ │ │ ├── sample.cbz │ │ │ └── sous-le-vent.epub │ │ └── favicon.ico │ ├── src │ │ ├── App.tsx │ │ ├── books │ │ │ ├── BookTable.tsx │ │ │ ├── BooksScreen.tsx │ │ │ ├── Glossary.tsx │ │ │ ├── NavigationBreadcrumb.tsx │ │ │ ├── UploadBook.tsx │ │ │ ├── constants.ts │ │ │ └── useUploadedBooks.ts │ │ ├── common │ │ │ ├── AppBar.tsx │ │ │ ├── Button.tsx │ │ │ ├── FullScreenDialog.tsx │ │ │ ├── NavigationSettings.tsx │ │ │ ├── OrDivider.tsx │ │ │ ├── OtherSettings.tsx │ │ │ ├── rxjs.ts │ │ │ └── utils.ts │ │ ├── components │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── breadcrumb.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── close-button.tsx │ │ │ │ ├── color-mode.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── drawer.tsx │ │ │ │ ├── field.tsx │ │ │ │ ├── input-group.tsx │ │ │ │ ├── number-input.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── provider.tsx │ │ │ │ ├── radio.tsx │ │ │ │ ├── slider.tsx │ │ │ │ ├── toaster.tsx │ │ │ │ └── tooltip.tsx │ │ ├── constants.shared.ts │ │ ├── home │ │ │ └── HomeScreen.tsx │ │ ├── main.tsx │ │ ├── reader │ │ │ ├── BookError.tsx │ │ │ ├── BookLoading.tsx │ │ │ ├── ReaderScreen.tsx │ │ │ ├── annotations │ │ │ │ ├── HighlightMenu.tsx │ │ │ │ ├── states.ts │ │ │ │ └── useAnnotations.ts │ │ │ ├── gestures │ │ │ │ └── useGestureHandler.ts │ │ │ ├── navigation │ │ │ │ ├── MenuDialog.tsx │ │ │ │ └── QuickActionsMenu.tsx │ │ │ ├── settings │ │ │ │ ├── SettingsMenu.tsx │ │ │ │ ├── useLocalSettings.ts │ │ │ │ ├── useReaderSettings.ts │ │ │ │ └── useUpdateReaderSettings.ts │ │ │ ├── states.ts │ │ │ ├── useBookmarks.ts │ │ │ ├── useCreateReader.ts │ │ │ ├── useManifest.ts │ │ │ ├── usePersistCurrentPage.ts │ │ │ └── useReader.ts │ │ ├── serviceWorker │ │ │ ├── service-worker.ts │ │ │ └── utils.ts │ │ ├── streamer │ │ │ ├── streamer.sw.ts │ │ │ ├── utils.shared.ts │ │ │ └── webStreamer.ts │ │ ├── theme │ │ │ └── theme.ts │ │ ├── types.ts │ │ └── vite-env.d.ts │ ├── stream-shim.js │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vercel.json │ └── vite.config.ts ├── front │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── robots.txt │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ ├── demo.prose-reader.com_books (4).png │ │ │ ├── demo.prose-reader.com_books (5).png │ │ │ ├── demo.prose-reader.com_books.png │ │ │ ├── header_logo.svg │ │ │ ├── make-it-yours.png │ │ │ └── react.svg │ │ ├── components │ │ │ ├── OrDivider.tsx │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── close-button.tsx │ │ │ │ ├── color-mode.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── drawer.tsx │ │ │ │ ├── field.tsx │ │ │ │ ├── input-group.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── provider.tsx │ │ │ │ ├── radio.tsx │ │ │ │ ├── slider.tsx │ │ │ │ └── tooltip.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── sections │ │ │ ├── DeveloperFriendlySection.tsx │ │ │ ├── ReadAnythingSection.tsx │ │ │ ├── Section.tsx │ │ │ └── ThemingSection.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vercel.json │ └── vite.config.ts └── tests │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── playwright.config.ts │ ├── public │ ├── epubs │ │ ├── accessible_epub_3.epub │ │ ├── haruko-html-jpeg.epub │ │ ├── rendition-flow-webtoon.epub │ │ ├── sample-3.pdf │ │ ├── sample.cbz │ │ └── sous-le-vent.epub │ └── images │ │ └── torinome.jpeg │ ├── tests │ ├── bookmarks │ │ ├── index.html │ │ ├── index.tsx │ │ └── pdf.spec.ts │ ├── layout │ │ └── webtoon │ │ │ ├── index.html │ │ │ ├── index.tsx │ │ │ └── spine-item-layout.test.ts │ ├── navigation │ │ ├── manual │ │ │ ├── comics │ │ │ │ ├── index.html │ │ │ │ ├── index.tsx │ │ │ │ └── turn.spec.ts │ │ │ └── rtl-haruko │ │ │ │ ├── index.html │ │ │ │ ├── index.tsx │ │ │ │ └── turn.spec.ts │ │ ├── restoration │ │ │ ├── epub │ │ │ │ ├── cfi.spec.ts │ │ │ │ ├── index.html │ │ │ │ └── index.tsx │ │ │ └── rtl-haruko │ │ │ │ ├── cfi.spec.ts │ │ │ │ ├── index.html │ │ │ │ └── index.tsx │ │ └── scrolling │ │ │ └── pdf │ │ │ ├── index.html │ │ │ ├── index.tsx │ │ │ ├── pdf.spec.ts │ │ │ └── pdf.spec.ts-snapshots │ │ │ ├── should-navigate-to-second-page-and-back-to-first-page-1-chromium-darwin.png │ │ │ ├── should-navigate-to-second-page-and-back-to-first-page-1-firefox-darwin.png │ │ │ ├── should-navigate-to-second-page-and-back-to-first-page-1-webkit-darwin.png │ │ │ ├── should-navigate-to-second-page-and-back-to-first-page-2-chromium-darwin.png │ │ │ ├── should-navigate-to-second-page-and-back-to-first-page-2-firefox-darwin.png │ │ │ ├── should-navigate-to-second-page-and-back-to-first-page-2-webkit-darwin.png │ │ │ ├── should-restore-to-first-page-with-CFI-1-chromium-darwin.png │ │ │ ├── should-restore-to-first-page-with-CFI-1-firefox-darwin.png │ │ │ ├── should-restore-to-first-page-with-CFI-1-webkit-darwin.png │ │ │ ├── should-restore-to-second-page-with-CFI-1-chromium-darwin.png │ │ │ ├── should-restore-to-second-page-with-CFI-1-firefox-darwin.png │ │ │ └── should-restore-to-second-page-with-CFI-1-webkit-darwin.png │ ├── pagination │ │ └── progression │ │ │ └── no-weight-manifest │ │ │ ├── index.html │ │ │ ├── index.tsx │ │ │ └── progression.test.ts │ ├── prepaginated │ │ ├── index.html │ │ ├── index.tsx │ │ ├── layout-spread.spec.ts │ │ └── layout-spread.spec.ts-snapshots │ │ │ ├── page-spread-right-chromium-darwin.jpg │ │ │ ├── page-spread-right-firefox-darwin.jpg │ │ │ ├── page-spread-right-webkit-darwin.jpg │ │ │ ├── right-navigation-layout-chromium-darwin.jpg │ │ │ ├── right-navigation-layout-firefox-darwin.jpg │ │ │ └── right-navigation-layout-webkit-darwin.jpg │ ├── text │ │ ├── index.html │ │ ├── index.tsx │ │ ├── text.spec.ts │ │ └── text.spec.ts-snapshots │ │ │ ├── text-chromium-darwin.png │ │ │ ├── text-firefox-darwin.png │ │ │ └── text-webkit-darwin.png │ └── utils.ts │ ├── tsconfig.json │ ├── vite-env.d.ts │ └── vite.config.ts ├── biome.json ├── lerna.json ├── nx.json ├── package-lock.json ├── package.json ├── packages ├── cfi │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── compare.test.ts │ │ ├── compare.ts │ │ ├── generate.range.test.ts │ │ ├── generate.test.ts │ │ ├── generate.ts │ │ ├── index.ts │ │ ├── parse.indirection.test.ts │ │ ├── parse.test.ts │ │ ├── parse.ts │ │ ├── resolve.range.test.ts │ │ ├── resolve.test.ts │ │ ├── resolve.ts │ │ ├── serialize.test.ts │ │ ├── serialize.ts │ │ ├── tests │ │ │ ├── test1.test.ts │ │ │ ├── test1.xhtml │ │ │ ├── test2.test.ts │ │ │ ├── test2.xhtml │ │ │ ├── test4.test.ts │ │ │ └── test4.xhtml │ │ ├── utils.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts ├── core │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── cfi │ │ │ ├── generate.test.ts │ │ │ ├── generate.ts │ │ │ ├── index.ts │ │ │ ├── parse.test.ts │ │ │ ├── parse.ts │ │ │ └── resolve.ts │ │ ├── constants.ts │ │ ├── context │ │ │ ├── BridgeEvent.ts │ │ │ ├── Context.ts │ │ │ └── isUsingSpreadMode.ts │ │ ├── createReaderWithEnhancer.ts │ │ ├── enhancers │ │ │ ├── accessibility.ts │ │ │ ├── chrome.ts │ │ │ ├── events │ │ │ │ ├── events.ts │ │ │ │ ├── normalizeEventForViewport.ts │ │ │ │ └── translateFramePositionIntoPage.ts │ │ │ ├── firefox.ts │ │ │ ├── fonts │ │ │ │ ├── SettingsManager.ts │ │ │ │ ├── fonts.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── hotkeys.ts │ │ │ ├── html │ │ │ │ ├── enhancer.ts │ │ │ │ ├── links.ts │ │ │ │ └── renderer │ │ │ │ │ ├── HtmlRenderer.ts │ │ │ │ │ ├── assets.ts │ │ │ │ │ ├── attachFrameSrc.ts │ │ │ │ │ ├── createFrameElement.ts │ │ │ │ │ ├── createHtmlPageFromResource.ts │ │ │ │ │ ├── prePaginated │ │ │ │ │ └── renderPrePaginated.ts │ │ │ │ │ └── reflowable │ │ │ │ │ ├── renderReflowable.ts │ │ │ │ │ └── styles.ts │ │ │ ├── layout │ │ │ │ ├── SettingsManager.ts │ │ │ │ ├── createMovingSafePan$.ts │ │ │ │ ├── fixReflowable.ts │ │ │ │ ├── layoutEnhancer.ts │ │ │ │ ├── layoutInfo.ts │ │ │ │ ├── types.ts │ │ │ │ └── viewportMode.ts │ │ │ ├── loading │ │ │ │ ├── constants.ts │ │ │ │ ├── createLoadingElement.ts │ │ │ │ └── loadingEnhancer.ts │ │ │ ├── media │ │ │ │ ├── ImageRenderer.ts │ │ │ │ └── media.ts │ │ │ ├── navigation │ │ │ │ ├── index.ts │ │ │ │ ├── links.ts │ │ │ │ ├── navigators │ │ │ │ │ ├── manualNavigator.ts │ │ │ │ │ └── panNavigator.ts │ │ │ │ ├── report.ts │ │ │ │ ├── resolvers │ │ │ │ │ ├── getNavigationForLeftOrTopPage.ts │ │ │ │ │ ├── getNavigationForLeftSinglePage.ts │ │ │ │ │ ├── getNavigationForRightOrBottomPage.ts │ │ │ │ │ ├── getNavigationForRightSinglePage.ts │ │ │ │ │ ├── getSpineItemPositionForLeftPage.ts │ │ │ │ │ └── getSpineItemPositionForRightPage.ts │ │ │ │ ├── state.ts │ │ │ │ ├── throttleLock.ts │ │ │ │ └── types.ts │ │ │ ├── pagination │ │ │ │ ├── ResourcesLocator.ts │ │ │ │ ├── chapters.ts │ │ │ │ ├── enhancer.ts │ │ │ │ ├── pagination.test.ts │ │ │ │ ├── pagination.ts │ │ │ │ ├── progression.ts │ │ │ │ ├── spine.ts │ │ │ │ └── types.ts │ │ │ ├── resources │ │ │ │ ├── index.ts │ │ │ │ ├── indexedDB.ts │ │ │ │ └── resourcesManager.ts │ │ │ ├── selection │ │ │ │ ├── FrameSelectionTracker.ts │ │ │ │ ├── selection.ts │ │ │ │ ├── selectionEnhancer.ts │ │ │ │ └── trackSpineItemSelection.ts │ │ │ ├── theme.ts │ │ │ ├── types │ │ │ │ └── enhancer.ts │ │ │ ├── utils.ts │ │ │ ├── webkit.ts │ │ │ └── zoom │ │ │ │ ├── ControllableZoomer.ts │ │ │ │ ├── ScrollableZoomer.ts │ │ │ │ ├── Zoomer.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ ├── features │ │ │ └── Features.ts │ │ ├── hooks │ │ │ ├── HookManager.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── manifest │ │ │ └── isFullyPrePaginated.ts │ │ ├── navigation │ │ │ ├── InternalNavigator.test.ts │ │ │ ├── InternalNavigator.ts │ │ │ ├── Locker.ts │ │ │ ├── Navigator.ts │ │ │ ├── UserScrollNavigation.ts │ │ │ ├── consolidation │ │ │ │ ├── consolidateWithPagination.ts │ │ │ │ ├── mapUserNavigationToInternal.ts │ │ │ │ ├── withCfiPosition.ts │ │ │ │ ├── withDirection.ts │ │ │ │ ├── withFallbackPosition.ts │ │ │ │ ├── withPaginationInfo.ts │ │ │ │ ├── withSpineItem.ts │ │ │ │ ├── withSpineItemLayoutInfo.ts │ │ │ │ ├── withSpineItemPosition.ts │ │ │ │ └── withUrlInfo.ts │ │ │ ├── controllers │ │ │ │ ├── ControlledNavigationController.ts │ │ │ │ ├── ScrollNavigationController.ts │ │ │ │ ├── getScaledDownPosition.ts │ │ │ │ └── positions.ts │ │ │ ├── resolvers │ │ │ │ ├── NavigationResolver.ts │ │ │ │ ├── getAdjustedPositionForSpread.ts │ │ │ │ ├── getAdjustedPositionWithSafeEdge.ts │ │ │ │ ├── getNavigationForPosition.ts │ │ │ │ ├── getNavigationForSpineItemPage.ts │ │ │ │ ├── getNavigationForUrl.ts │ │ │ │ └── getNavigationFromSpineItemPosition.ts │ │ │ ├── restoration │ │ │ │ ├── restoreNavigationForControlledPageTurnMode.test.ts │ │ │ │ ├── restoreNavigationForControlledPageTurnMode.ts │ │ │ │ ├── restorePosition.ts │ │ │ │ └── withRestoredPosition.ts │ │ │ ├── tests │ │ │ │ └── utils.ts │ │ │ └── types.ts │ │ ├── pagination │ │ │ ├── Pagination.ts │ │ │ ├── PaginationController.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── reader.ts │ │ ├── report.ts │ │ ├── settings │ │ │ ├── ReaderSettingsManager.ts │ │ │ ├── SettingsInterface.ts │ │ │ ├── SettingsManager.ts │ │ │ ├── SettingsManagerOverload.ts │ │ │ └── types.ts │ │ ├── spine │ │ │ ├── Spine.ts │ │ │ ├── SpineItemsManager.ts │ │ │ ├── SpineItemsObserver.ts │ │ │ ├── SpineLayout.ts │ │ │ ├── layout │ │ │ │ └── layoutItem.ts │ │ │ ├── loader │ │ │ │ └── SpineItemsLoader.ts │ │ │ ├── locator │ │ │ │ ├── SpineLocator.ts │ │ │ │ ├── getAbsolutePageIndexFromPageIndex.ts │ │ │ │ ├── getItemVisibilityForPosition.test.ts │ │ │ │ ├── getItemVisibilityForPosition.ts │ │ │ │ ├── getSpineInfoFromAbsolutePageIndex.ts │ │ │ │ ├── getSpineItemFromPosition.ts │ │ │ │ ├── getSpinePositionFromSpineItemPageIndex.ts │ │ │ │ ├── getSpinePositionFromSpineItemPosition.ts │ │ │ │ ├── getVisibleSpineItemsFromPosition.test.ts │ │ │ │ └── getVisibleSpineItemsFromPosition.ts │ │ │ └── types.ts │ │ ├── spineItem │ │ │ ├── SpineItem.ts │ │ │ ├── SpineItemLayout.ts │ │ │ ├── helpers.ts │ │ │ ├── layout │ │ │ │ ├── getSpineItemNumberOfPages.ts │ │ │ │ ├── getSpineItemPagesPosition.ts │ │ │ │ └── getSpineItemPositionFromPageIndex.ts │ │ │ ├── locationResolver.ts │ │ │ ├── navigationResolver.ts │ │ │ ├── renderer │ │ │ │ ├── DefaultRenderer.ts │ │ │ │ └── DocumentRenderer.ts │ │ │ ├── resources │ │ │ │ └── ResourceHandler.ts │ │ │ └── types.ts │ │ ├── tests │ │ │ └── utils.ts │ │ ├── types │ │ │ └── index.d.ts │ │ ├── utils │ │ │ ├── DestroyableClass.ts │ │ │ ├── ReactiveEntity.ts │ │ │ ├── dom.ts │ │ │ ├── frames.ts │ │ │ ├── isDefined.ts │ │ │ ├── layout.test.ts │ │ │ ├── layout.ts │ │ │ ├── manifest.ts │ │ │ ├── objects.test.ts │ │ │ ├── objects.ts │ │ │ └── rxjs.ts │ │ └── viewport │ │ │ ├── Viewport.ts │ │ │ ├── translateSpinePositionToRelativeViewport.ts │ │ │ └── types.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts ├── enhancer-annotations │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── Commands.ts │ │ ├── annotationsEnhancer.ts │ │ ├── highlights │ │ │ ├── Highlight.ts │ │ │ ├── ReaderHighlights.ts │ │ │ ├── SpineItemHighlight.ts │ │ │ ├── SpineItemHighlights.ts │ │ │ ├── consolidate.ts │ │ │ └── utils.ts │ │ ├── index.ts │ │ ├── report.ts │ │ └── types.ts │ ├── tsconfig.json │ └── vite.config.ts ├── enhancer-bookmarks │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── Commands.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── report.ts │ │ └── types.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts ├── enhancer-gallery │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── Snapshot.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── copyIframeContents.ts │ │ │ ├── deepCloneElement.ts │ │ │ └── redrawCanvas.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts ├── enhancer-gestures │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── SettingsManager.ts │ │ ├── gestures │ │ │ ├── pan.ts │ │ │ ├── pinch.ts │ │ │ ├── swipe.ts │ │ │ ├── taps.ts │ │ │ └── zoomPan.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── vite.config.js │ └── vitest.config.ts ├── enhancer-pdf │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── createArchiveFromPdf.ts │ │ ├── index.ts │ │ ├── pdfEnhancer.ts │ │ ├── renderer │ │ │ ├── PdfRenderer.ts │ │ │ ├── frame.css │ │ │ └── layout.ts │ │ └── types.ts │ ├── tsconfig.json │ └── vite.config.ts ├── enhancer-search │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── report.ts │ │ ├── search.ts │ │ └── types.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts ├── react-native │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ ├── native │ │ │ ├── ReactNativeStreamer.ts │ │ │ ├── ReaderProvider.tsx │ │ │ ├── createArchiveFromExpoFileSystemNext.ts │ │ │ ├── index.ts │ │ │ ├── useCreateReader.ts │ │ │ ├── useProseBridge.ts │ │ │ ├── useReader.ts │ │ │ └── useReaderState.ts │ │ ├── shared │ │ │ ├── index.ts │ │ │ └── useLiveRef.ts │ │ └── web │ │ │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── tsconfig.rn.json │ ├── tsconfig.web.json │ ├── vite.config.ts │ └── vitest.config.ts ├── react-reader │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── ReactReader.tsx │ │ ├── annotations │ │ │ ├── AnnotationsDialog.tsx │ │ │ └── AnnotationsDialogContent.tsx │ │ ├── bookmarks │ │ │ ├── Bookmarks.tsx │ │ │ ├── BookmarksDialog.tsx │ │ │ └── BookmarksDialogContent.tsx │ │ ├── common │ │ │ ├── useFullscreen.ts │ │ │ └── useMeasure.ts │ │ ├── components │ │ │ └── ui │ │ │ │ ├── accordion.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── close-button.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── drawer.tsx │ │ │ │ ├── field.tsx │ │ │ │ ├── input-group.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── progress.tsx │ │ │ │ ├── radio.tsx │ │ │ │ ├── slider.tsx │ │ │ │ ├── toaster.tsx │ │ │ │ ├── toggle-tip.tsx │ │ │ │ └── tooltip.tsx │ │ ├── context │ │ │ ├── ReactReaderProvider.tsx │ │ │ ├── context.ts │ │ │ ├── useReader.ts │ │ │ └── useReaderContext.ts │ │ ├── gallery │ │ │ ├── GalleryDialog.tsx │ │ │ └── useAttachSnapshot.ts │ │ ├── help │ │ │ └── HelpDialog.tsx │ │ ├── index.ts │ │ ├── navigation │ │ │ ├── FloatingProgress.tsx │ │ │ ├── FloatingTime.tsx │ │ │ ├── useInterceptExternalLinks.ts │ │ │ └── useNavigationContext.ts │ │ ├── notifications │ │ │ ├── types.ts │ │ │ └── useNotifications.ts │ │ ├── pagination │ │ │ └── usePagination.ts │ │ ├── quickmenu │ │ │ ├── BottomBar.tsx │ │ │ ├── PaginationInfoSection.tsx │ │ │ ├── QuickBar.tsx │ │ │ ├── QuickMenu.tsx │ │ │ ├── Scrubber.tsx │ │ │ ├── ThemedSlider.tsx │ │ │ ├── TimeIndicator.tsx │ │ │ ├── TopBar.tsx │ │ │ └── useQuickMenu.ts │ │ ├── search │ │ │ ├── SearchDialog.tsx │ │ │ ├── SearchDialogContent.tsx │ │ │ └── SearchListItem.tsx │ │ ├── settings │ │ │ └── useSettings.ts │ │ ├── theme │ │ │ └── config.ts │ │ ├── toc │ │ │ ├── TableOfContentsDialog.tsx │ │ │ └── TableOfContentsDialogContent.tsx │ │ ├── vite-env.d.ts │ │ └── zoom │ │ │ └── useNotifyZoom.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── shared │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── Manifest.ts │ │ ├── array.ts │ │ ├── contentType.ts │ │ ├── index.ts │ │ ├── objects.test.ts │ │ ├── objects.ts │ │ ├── report.ts │ │ ├── resources.ts │ │ ├── url.test.ts │ │ └── url.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts └── streamer │ ├── .gitignore │ ├── package.json │ ├── src │ ├── ServiceWorkerStreamer.test.ts │ ├── ServiceWorkerStreamer.ts │ ├── Streamer.test.ts │ ├── Streamer.ts │ ├── archives │ │ ├── archiveLoader.test.ts │ │ ├── archiveLoader.ts │ │ ├── createArchiveFromArrayBufferList.ts │ │ ├── createArchiveFromJszip.ts │ │ ├── createArchiveFromLibArchive.ts │ │ ├── createArchiveFromText.ts │ │ ├── createArchiveFromUrls.ts │ │ └── types.ts │ ├── configure.ts │ ├── epubs │ │ ├── getArchiveOpfInfo.ts │ │ ├── getSpineItemFilesFromArchive.ts │ │ └── isArchiveEpub.ts │ ├── generators │ │ ├── manifest │ │ │ ├── hooks │ │ │ │ ├── apple.ts │ │ │ │ ├── comicInfo.ts │ │ │ │ ├── default.ts │ │ │ │ ├── epub │ │ │ │ │ ├── epub.ts │ │ │ │ │ └── spineItems.ts │ │ │ │ ├── epubOptimizer.test.ts │ │ │ │ ├── epubOptimizer.ts │ │ │ │ ├── kobo.ts │ │ │ │ ├── navigationFallback.test.ts │ │ │ │ ├── navigationFallback.ts │ │ │ │ └── nonEpub.ts │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ └── resources │ │ │ ├── hooks │ │ │ ├── calibreFixHook.test.ts │ │ │ ├── calibreFixHook.ts │ │ │ ├── cssFixHook.ts │ │ │ ├── defaultHook.ts │ │ │ ├── selfClosingTagsFixHook.test.ts │ │ │ ├── selfClosingTagsFixHook.ts │ │ │ └── types.ts │ │ │ └── index.ts │ ├── index.ts │ ├── parsers │ │ ├── kobo.ts │ │ ├── nav.test.ts │ │ ├── nav.ts │ │ └── xml.ts │ ├── report.ts │ ├── tests │ │ ├── setupTests.ts │ │ ├── tocWithPrefix │ │ │ ├── content.opf │ │ │ ├── toc.json │ │ │ └── toc.ncx │ │ └── waitFor.ts │ └── utils │ │ ├── sortByTitleComparator.test.ts │ │ ├── sortByTitleComparator.ts │ │ └── uri.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts ├── prose-react-native-demo ├── .gitignore ├── .vscode │ └── settings.json ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ ├── debug.keystore │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── mbret │ │ │ │ └── prosereaderreactnativedemo │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainApplication.kt │ │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── splashscreen_logo.png │ │ │ ├── drawable-mdpi │ │ │ └── splashscreen_logo.png │ │ │ ├── drawable-xhdpi │ │ │ └── splashscreen_logo.png │ │ │ ├── drawable-xxhdpi │ │ │ └── splashscreen_logo.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── splashscreen_logo.png │ │ │ ├── drawable │ │ │ ├── ic_launcher_background.xml │ │ │ └── rn_edit_text_material.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_foreground.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_foreground.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_foreground.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_foreground.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_foreground.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── values-night │ │ │ └── colors.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── app.json ├── app │ ├── _layout.tsx │ └── index.tsx ├── assets │ ├── fonts │ │ └── SpaceMono-Regular.ttf │ ├── images │ │ ├── adaptive-icon.png │ │ ├── favicon.png │ │ ├── icon.png │ │ ├── partial-react-logo.png │ │ ├── react-logo.png │ │ ├── react-logo@2x.png │ │ ├── react-logo@3x.png │ │ └── splash-icon.png │ └── index.html ├── components │ ├── constants.ts │ ├── reader │ │ ├── BottomMenu.tsx │ │ ├── Reader.tsx │ │ ├── Streamer.ts │ │ ├── TopMenu.tsx │ │ └── useWebviewHtmlAsset.ts │ ├── useColorScheme.ts │ ├── useDownloadFile.ts │ └── useUnzipFile.ts ├── eslint.config.js ├── ios │ ├── .gitignore │ ├── .xcode.env │ ├── Podfile │ ├── Podfile.lock │ ├── Podfile.properties.json │ ├── prosereactnativedemo.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── prosereactnativedemo.xcscheme │ ├── prosereactnativedemo.xcworkspace │ │ └── contents.xcworkspacedata │ └── prosereactnativedemo │ │ ├── AppDelegate.swift │ │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── App-Icon-1024x1024@1x.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── SplashScreenBackground.colorset │ │ │ └── Contents.json │ │ └── SplashScreenLogo.imageset │ │ │ ├── Contents.json │ │ │ ├── image.png │ │ │ ├── image@2x.png │ │ │ └── image@3x.png │ │ ├── Info.plist │ │ ├── PrivacyInfo.xcprivacy │ │ ├── SplashScreen.storyboard │ │ ├── Supporting │ │ └── Expo.plist │ │ ├── prosereactnativedemo-Bridging-Header.h │ │ └── prosereactnativedemo.entitlements ├── metro.config.js ├── package-lock.json ├── package.json ├── scripts │ └── sync-packages.sh ├── string_decoder.js ├── tsconfig.json └── web │ ├── .gitignore │ ├── index.html │ ├── src │ ├── main.ts │ ├── style.css │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── tsconfig.node.json └── tsconfig.web.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mbret] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | time: '01:00' 8 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /.github/workflows/_lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | lint: 8 | runs-on: macos-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 22 14 | cache: 'npm' 15 | - name: Check 16 | run: | 17 | npm ci 18 | npm run format 19 | npm run lint 20 | -------------------------------------------------------------------------------- /.github/workflows/_test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | test: 8 | runs-on: macos-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 22 14 | cache: 'npm' 15 | 16 | - name: Install dependencies 17 | run: npm ci 18 | 19 | - uses: actions/cache@v4 20 | id: nx-cache 21 | with: 22 | path: | 23 | .nx 24 | key: ${{ runner.os }}-nx-${{ hashFiles('**/package-lock.json') }} 25 | 26 | - name: Build 27 | run: | 28 | npx lerna run build --stream --scope @prose-reader/* 29 | 30 | - name: Install Playwright Browsers 31 | run: npx playwright install --with-deps 32 | 33 | - name: Test 34 | run: | 35 | npm test 36 | 37 | - uses: actions/upload-artifact@v4 38 | if: ${{ !cancelled() }} 39 | with: 40 | name: playwright-report 41 | path: packages/tests/test-results/ 42 | retention-days: 15 -------------------------------------------------------------------------------- /.github/workflows/_ts.yml: -------------------------------------------------------------------------------- 1 | name: ts 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | ts: 8 | runs-on: macos-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 22 14 | cache: 'npm' 15 | - name: Check 16 | run: | 17 | npm ci 18 | npm run build 19 | npm run tsc 20 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'master' 7 | pull_request: 8 | branches-ignore: 9 | - 'master' 10 | 11 | jobs: 12 | lint: 13 | uses: ./.github/workflows/_lint.yml 14 | 15 | ts: 16 | uses: ./.github/workflows/_ts.yml 17 | 18 | tests: 19 | uses: ./.github/workflows/_test.yml 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | **/.test/coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | lerna-debug.log 26 | .nx/cache 27 | .nx/workspace-data 28 | .nx -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "files.exclude": { 4 | "**/dist": false, 5 | "**/build": true 6 | }, 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.biome": "explicit" 9 | }, 10 | "editor.defaultFormatter": "biomejs.biome", 11 | "[typescript]": { 12 | "editor.defaultFormatter": "biomejs.biome" 13 | }, 14 | "[typescriptreact]": { 15 | "editor.defaultFormatter": "biomejs.biome" 16 | }, 17 | "[github-actions-workflow]": { 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2021 @prose-reader/core 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 4 | Logo 5 | 6 | 7 |

prose

8 | 9 |

10 | Your free and open-source Reading System Engine. 11 |
Build your reading system hassle free with a modern and easy to use SDK. 12 |
13 | Website 14 | · 15 | Report bug 16 | · 17 | Request feature 18 |

19 | 20 |

21 | Official engine of oboku, a modern and free Reading App. 22 |
23 | Come take a look! 24 |

25 |

26 | -------------------------------------------------------------------------------- /apps/demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | dist 25 | 26 | public/epubs/* 27 | public/service-worker.js 28 | public/service-worker.js.map 29 | !public/epubs/accessible_epub_3.epub 30 | !public/epubs/haruko-html-jpeg.epub 31 | !public/epubs/moby-dick_txt.txt 32 | !public/epubs/Accessibility-Tests-Mathematics.epub 33 | !public/epubs/mymedia_lite.epub 34 | !public/epubs/sample.cbz 35 | !public/epubs/regime-anticancer-arabic.epub 36 | !public/epubs/sous-le-vent.epub 37 | !public/epubs/haruko-comic.zip 38 | !public/epubs/cc-shared-culture.epub 39 | !public/epubs/rendition-flow-webtoon-one-page.epub 40 | !public/epubs/rendition-flow-webtoon.epub 41 | !public/epubs/sample-3.pdf -------------------------------------------------------------------------------- /apps/demo/public/epubs/Accessibility-Tests-Mathematics.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/demo/public/epubs/Accessibility-Tests-Mathematics.epub -------------------------------------------------------------------------------- /apps/demo/public/epubs/accessible_epub_3.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/demo/public/epubs/accessible_epub_3.epub -------------------------------------------------------------------------------- /apps/demo/public/epubs/cc-shared-culture.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/demo/public/epubs/cc-shared-culture.epub -------------------------------------------------------------------------------- /apps/demo/public/epubs/haruko-comic.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/demo/public/epubs/haruko-comic.zip -------------------------------------------------------------------------------- /apps/demo/public/epubs/haruko-html-jpeg.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/demo/public/epubs/haruko-html-jpeg.epub -------------------------------------------------------------------------------- /apps/demo/public/epubs/mymedia_lite.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/demo/public/epubs/mymedia_lite.epub -------------------------------------------------------------------------------- /apps/demo/public/epubs/regime-anticancer-arabic.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/demo/public/epubs/regime-anticancer-arabic.epub -------------------------------------------------------------------------------- /apps/demo/public/epubs/rendition-flow-webtoon-one-page.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/demo/public/epubs/rendition-flow-webtoon-one-page.epub -------------------------------------------------------------------------------- /apps/demo/public/epubs/rendition-flow-webtoon.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/demo/public/epubs/rendition-flow-webtoon.epub -------------------------------------------------------------------------------- /apps/demo/public/epubs/sample-3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/demo/public/epubs/sample-3.pdf -------------------------------------------------------------------------------- /apps/demo/public/epubs/sample.cbz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/demo/public/epubs/sample.cbz -------------------------------------------------------------------------------- /apps/demo/public/epubs/sous-le-vent.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/demo/public/epubs/sous-le-vent.epub -------------------------------------------------------------------------------- /apps/demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/demo/public/favicon.ico -------------------------------------------------------------------------------- /apps/demo/src/books/Glossary.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, Text } from "@chakra-ui/react" 2 | 3 | export const Glossary = () => ( 4 | 5 | 6 | LTR = left to right, RTL = right to left 7 | 8 | 9 | RFL = fully reflowable 10 | 11 | 12 | RFL(P) = partially reflowable 13 | 14 | 15 | FXL = fully pre-paginated (fixed layout) 16 | 17 | 18 | FXL(P) = partially pre-paginated (fixed layout) 19 | 20 | 21 | TXT = .txt file (RFL) 22 | 23 | 24 | MEDIA = contains media (audio, video) 25 | 26 | 27 | ) 28 | -------------------------------------------------------------------------------- /apps/demo/src/books/NavigationBreadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router" 2 | import { 3 | BreadcrumbCurrentLink, 4 | BreadcrumbLink, 5 | BreadcrumbRoot, 6 | } from "../components/ui/breadcrumb" 7 | 8 | export const NavigationBreadcrumb = () => { 9 | return ( 10 | 11 | 12 | Home 13 | 14 | 15 | 16 | Books 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /apps/demo/src/books/useUploadedBooks.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query" 2 | import localforage from "localforage" 3 | 4 | export const useUploadedBooks = () => { 5 | return useQuery({ 6 | queryKey: ["uploadedBooks"], 7 | queryFn: async () => { 8 | const keys = await localforage.keys() 9 | 10 | return keys.map((name) => { 11 | return { 12 | name, 13 | base64Uri: btoa(`${encodeURIComponent(`file://epubs/${name}`)}`), 14 | } 15 | }) 16 | }, 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /apps/demo/src/common/AppBar.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, Text } from "@chakra-ui/react" 2 | import type React from "react" 3 | import { type ComponentProps, memo } from "react" 4 | 5 | export const AppBar = memo( 6 | ({ 7 | leftElement, 8 | middleElement, 9 | rightElement, 10 | ...rest 11 | }: ComponentProps & { 12 | leftElement?: React.ReactElement 13 | middleElement?: React.ReactElement | string 14 | rightElement?: React.ReactElement 15 | }) => ( 16 | 26 | {leftElement} 27 | {typeof middleElement === "string" ? ( 28 | 29 | {middleElement} 30 | 31 | ) : ( 32 | middleElement 33 | )} 34 | {rightElement} 35 | 36 | ), 37 | ) 38 | -------------------------------------------------------------------------------- /apps/demo/src/common/Button.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react" 2 | 3 | export const Button = ({ 4 | children, 5 | style, 6 | ...rest 7 | }: React.DetailedHTMLProps< 8 | React.ButtonHTMLAttributes, 9 | HTMLButtonElement 10 | >) => { 11 | return ( 12 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /apps/demo/src/common/OrDivider.tsx: -------------------------------------------------------------------------------- 1 | import { Box, type BoxProps, Text } from "@chakra-ui/react" 2 | import type React from "react" 3 | import type { FC } from "react" 4 | 5 | export const OrDivider: FC< 6 | { title?: string; style?: React.CSSProperties } & BoxProps 7 | > = ({ title = "or", ...rest }) => { 8 | return ( 9 | 16 | 24 | 25 | {title} 26 | 27 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /apps/demo/src/common/rxjs.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react" 2 | import { type Observable, Subject, filter } from "rxjs" 3 | 4 | export const useUnMount$ = () => { 5 | const unMount$ = useRef(new Subject()) 6 | 7 | useEffect(() => { 8 | return () => { 9 | unMount$.current.next() 10 | unMount$.current.complete() 11 | } 12 | }, []) 13 | 14 | return unMount$.current.asObservable() 15 | } 16 | 17 | function inputIsNotNullOrUndefined(input: null | undefined | T): input is T { 18 | return input !== null && input !== undefined 19 | } 20 | 21 | export function isNotNullOrUndefined() { 22 | return (source$: Observable) => 23 | source$.pipe(filter(inputIsNotNullOrUndefined)) 24 | } 25 | -------------------------------------------------------------------------------- /apps/demo/src/common/utils.ts: -------------------------------------------------------------------------------- 1 | export const truncateText = (text: string, maxLength = 50): string => { 2 | if (text.length <= maxLength) return text 3 | return `${text.slice(0, maxLength)}...` 4 | } 5 | -------------------------------------------------------------------------------- /apps/demo/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox as ChakraCheckbox } from "@chakra-ui/react" 2 | import * as React from "react" 3 | 4 | export interface CheckboxProps extends ChakraCheckbox.RootProps { 5 | icon?: React.ReactNode 6 | inputProps?: React.InputHTMLAttributes 7 | rootRef?: React.Ref 8 | } 9 | 10 | export const Checkbox = React.forwardRef( 11 | function Checkbox(props, ref) { 12 | const { icon, children, inputProps, rootRef, ...rest } = props 13 | return ( 14 | 15 | 16 | 17 | {icon || } 18 | 19 | {children != null && ( 20 | {children} 21 | )} 22 | 23 | ) 24 | }, 25 | ) 26 | -------------------------------------------------------------------------------- /apps/demo/src/components/ui/close-button.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonProps } from "@chakra-ui/react" 2 | import { IconButton as ChakraIconButton } from "@chakra-ui/react" 3 | import * as React from "react" 4 | import { LuX } from "react-icons/lu" 5 | 6 | export type CloseButtonProps = ButtonProps 7 | 8 | export const CloseButton = React.forwardRef< 9 | HTMLButtonElement, 10 | CloseButtonProps 11 | >(function CloseButton(props, ref) { 12 | return ( 13 | 14 | {props.children ?? } 15 | 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /apps/demo/src/components/ui/number-input.tsx: -------------------------------------------------------------------------------- 1 | import { NumberInput as ChakraNumberInput } from "@chakra-ui/react" 2 | import * as React from "react" 3 | 4 | export interface NumberInputProps extends ChakraNumberInput.RootProps {} 5 | 6 | export const NumberInputRoot = React.forwardRef< 7 | HTMLDivElement, 8 | NumberInputProps 9 | >(function NumberInput(props, ref) { 10 | const { children, ...rest } = props 11 | return ( 12 | 13 | {children} 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | }) 21 | 22 | export const NumberInputField = ChakraNumberInput.Input 23 | export const NumberInputScrubber = ChakraNumberInput.Scrubber 24 | export const NumberInputLabel = ChakraNumberInput.Label 25 | -------------------------------------------------------------------------------- /apps/demo/src/components/ui/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ChakraProvider } from "@chakra-ui/react" 4 | import system from "../../theme/theme" 5 | import { ColorModeProvider, type ColorModeProviderProps } from "./color-mode" 6 | 7 | export function Provider(props: ColorModeProviderProps) { 8 | return ( 9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /apps/demo/src/components/ui/radio.tsx: -------------------------------------------------------------------------------- 1 | import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react" 2 | import * as React from "react" 3 | 4 | export interface RadioProps extends ChakraRadioGroup.ItemProps { 5 | rootRef?: React.Ref 6 | inputProps?: React.InputHTMLAttributes 7 | } 8 | 9 | export const Radio = React.forwardRef( 10 | function Radio(props, ref) { 11 | const { children, inputProps, rootRef, ...rest } = props 12 | return ( 13 | 14 | 15 | 16 | {children && ( 17 | {children} 18 | )} 19 | 20 | ) 21 | }, 22 | ) 23 | 24 | export const RadioGroup = ChakraRadioGroup.Root 25 | -------------------------------------------------------------------------------- /apps/demo/src/constants.shared.ts: -------------------------------------------------------------------------------- 1 | export const PROJECT_NAME = `@prose/web-reader` 2 | export const FONT_SCALE_MIN = 0.2 3 | export const FONT_SCALE_MAX = 5 4 | export const STREAMER_URL_PREFIX = `streamer` 5 | -------------------------------------------------------------------------------- /apps/demo/src/reader/BookError.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from "@chakra-ui/react" 2 | 3 | export const BookError = ({ url }: { url: string }) => { 4 | return ( 5 | 22 | Unable to load your book {url} 23 | 24 | Make sure to have CORS enabled if you are linking to an external 25 | resource 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /apps/demo/src/reader/BookLoading.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, Text } from "@chakra-ui/react" 2 | 3 | export const BookLoading = () => { 4 | return ( 5 | 18 | Loading book 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /apps/demo/src/reader/annotations/states.ts: -------------------------------------------------------------------------------- 1 | import type { ProseHighlight } from "@prose-reader/enhancer-annotations" 2 | import { signal } from "reactjrx" 3 | 4 | export const selectedHighlightSignal = signal<{ 5 | highlight?: ProseHighlight 6 | selection?: { 7 | selection: Selection 8 | itemIndex: number 9 | } 10 | }>({ 11 | key: `selectedHighlightSignal`, 12 | default: {}, 13 | }) 14 | -------------------------------------------------------------------------------- /apps/demo/src/reader/settings/useLocalSettings.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | 3 | export type LocalSettings = { 4 | enablePan: boolean 5 | enableSwipe: boolean 6 | } 7 | 8 | export const useLocalSettings = (defaultSettings: Partial) => { 9 | return useState({ 10 | enablePan: true, 11 | enableSwipe: false, 12 | ...defaultSettings, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /apps/demo/src/reader/settings/useReaderSettings.ts: -------------------------------------------------------------------------------- 1 | import { useObserve } from "reactjrx" 2 | import { of } from "rxjs" 3 | import { useReader } from "../useReader" 4 | 5 | export const useReaderSettings = () => { 6 | const { reader } = useReader() 7 | 8 | return useObserve(() => reader?.settings.values$ ?? of(undefined), [reader]) 9 | } 10 | -------------------------------------------------------------------------------- /apps/demo/src/reader/usePersistCurrentPage.ts: -------------------------------------------------------------------------------- 1 | import { useSubscribe } from "reactjrx" 2 | import { useReader } from "./useReader" 3 | 4 | export const usePersistCurrentPagination = () => { 5 | const { reader } = useReader() 6 | 7 | useSubscribe( 8 | () => 9 | reader?.pagination.state$.subscribe(({ beginCfi = `` }) => { 10 | localStorage.setItem(`cfi`, beginCfi) 11 | }), 12 | [reader], 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /apps/demo/src/reader/useReader.ts: -------------------------------------------------------------------------------- 1 | import { signal, useSignalValue } from "reactjrx" 2 | import type { ReaderInstance } from "./useCreateReader" 3 | 4 | export const readerSignal = signal({}) 5 | 6 | export const useReader = () => { 7 | const reader = useSignalValue(readerSignal) 8 | 9 | // @ts-ignore 10 | window.reader = reader 11 | 12 | return { reader } 13 | } 14 | -------------------------------------------------------------------------------- /apps/demo/src/serviceWorker/utils.ts: -------------------------------------------------------------------------------- 1 | export const getEpubFilenameFromUrl = (url: string) => { 2 | return url.substring(url.lastIndexOf("/") + 1) 3 | } 4 | -------------------------------------------------------------------------------- /apps/demo/src/streamer/streamer.sw.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ServiceWorkerStreamer, 3 | createArchiveFromJszip, 4 | createArchiveFromText, 5 | } from "@prose-reader/streamer" 6 | import { loadAsync } from "jszip" 7 | import { STREAMER_URL_PREFIX } from "../constants.shared" 8 | import { getBlobFromKey, getStreamerBaseUrl } from "./utils.shared" 9 | 10 | export const swStreamer = new ServiceWorkerStreamer({ 11 | cleanArchiveAfter: 5 * 60 * 1000, 12 | getUriInfo: (event) => { 13 | const url = new URL(event.request.url) 14 | 15 | if (!url.pathname.startsWith(`/${STREAMER_URL_PREFIX}`)) { 16 | return undefined 17 | } 18 | 19 | return { baseUrl: getStreamerBaseUrl(url) } 20 | }, 21 | getArchive: async (key) => { 22 | const { blob, url } = await getBlobFromKey(key) 23 | 24 | if (url.endsWith(`.txt`)) { 25 | return await createArchiveFromText(blob) 26 | } 27 | const name = blob.name 28 | const jszip = await loadAsync(blob) 29 | 30 | return await createArchiveFromJszip(jszip, { orderByAlpha: true, name }) 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /apps/demo/src/theme/theme.ts: -------------------------------------------------------------------------------- 1 | import { createSystem, defaultConfig, defineConfig } from "@chakra-ui/react" 2 | 3 | export const config = defineConfig({ 4 | theme: { 5 | tokens: { 6 | fonts: { 7 | body: { 8 | value: "Roboto, Roboto Fallback", 9 | }, 10 | }, 11 | }, 12 | }, 13 | }) 14 | 15 | export default createSystem(defaultConfig, config) 16 | -------------------------------------------------------------------------------- /apps/demo/src/types.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | __PROSE_READER_DEBUG?: boolean | string 4 | } 5 | } 6 | 7 | export {} 8 | -------------------------------------------------------------------------------- /apps/demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /apps/demo/stream-shim.js: -------------------------------------------------------------------------------- 1 | export function Stream() { 2 | console.log("Stream") 3 | } 4 | 5 | export const stream = { 6 | Stream: function Stream() {}, 7 | } 8 | 9 | stream.Stream.prototype = Stream.prototype 10 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | "strict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noUncheckedSideEffectImports": true, 20 | "noUncheckedIndexedAccess": true 21 | }, 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "isolatedModules": true, 11 | "moduleDetection": "force", 12 | "noEmit": true, 13 | "strict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noUncheckedSideEffectImports": true 18 | }, 19 | "include": ["vite.config.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/demo/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }] 3 | } 4 | -------------------------------------------------------------------------------- /apps/front/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /apps/front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prose-reader-front", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@chakra-ui/react": "^3.2.3", 13 | "@emotion/react": "^11.14.0", 14 | "next-themes": "^0.4.4", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0", 17 | "react-icons": "^5.4.0" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^19.0.2", 21 | "@types/react-dom": "^19.0.0", 22 | "@vitejs/plugin-react": "^4.3.4", 23 | "typescript": "*", 24 | "vite": "^6.0.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/front/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/front/public/favicon.ico -------------------------------------------------------------------------------- /apps/front/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / -------------------------------------------------------------------------------- /apps/front/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/front/src/App.css -------------------------------------------------------------------------------- /apps/front/src/assets/demo.prose-reader.com_books (4).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/front/src/assets/demo.prose-reader.com_books (4).png -------------------------------------------------------------------------------- /apps/front/src/assets/demo.prose-reader.com_books (5).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/front/src/assets/demo.prose-reader.com_books (5).png -------------------------------------------------------------------------------- /apps/front/src/assets/demo.prose-reader.com_books.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/front/src/assets/demo.prose-reader.com_books.png -------------------------------------------------------------------------------- /apps/front/src/assets/make-it-yours.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/front/src/assets/make-it-yours.png -------------------------------------------------------------------------------- /apps/front/src/components/OrDivider.tsx: -------------------------------------------------------------------------------- 1 | import { Box, type BoxProps, Text } from "@chakra-ui/react" 2 | import type React from "react" 3 | import type { FC } from "react" 4 | 5 | export const OrDivider: FC< 6 | { title?: string; style?: React.CSSProperties } & BoxProps 7 | > = ({ title = "or", ...rest }) => { 8 | return ( 9 | 17 | 25 | 26 | {title} 27 | 28 | 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /apps/front/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox as ChakraCheckbox } from "@chakra-ui/react" 2 | import * as React from "react" 3 | 4 | export interface CheckboxProps extends ChakraCheckbox.RootProps { 5 | icon?: React.ReactNode 6 | inputProps?: React.InputHTMLAttributes 7 | rootRef?: React.Ref 8 | } 9 | 10 | export const Checkbox = React.forwardRef( 11 | function Checkbox(props, ref) { 12 | const { icon, children, inputProps, rootRef, ...rest } = props 13 | return ( 14 | 15 | 16 | 17 | {icon || } 18 | 19 | {children != null && ( 20 | {children} 21 | )} 22 | 23 | ) 24 | }, 25 | ) 26 | -------------------------------------------------------------------------------- /apps/front/src/components/ui/close-button.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonProps } from "@chakra-ui/react" 2 | import { IconButton as ChakraIconButton } from "@chakra-ui/react" 3 | import * as React from "react" 4 | import { LuX } from "react-icons/lu" 5 | 6 | export type CloseButtonProps = ButtonProps 7 | 8 | export const CloseButton = React.forwardRef< 9 | HTMLButtonElement, 10 | CloseButtonProps 11 | >(function CloseButton(props, ref) { 12 | return ( 13 | 14 | {props.children ?? } 15 | 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /apps/front/src/components/ui/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | ChakraProvider, 5 | createSystem, 6 | defaultConfig, 7 | defineConfig, 8 | } from "@chakra-ui/react" 9 | import { ColorModeProvider, type ColorModeProviderProps } from "./color-mode" 10 | 11 | const config = defineConfig({ 12 | globalCss: { 13 | html: { 14 | colorPalette: "red", 15 | }, 16 | }, 17 | theme: { 18 | tokens: { 19 | fonts: { 20 | body: { 21 | value: "Roboto, Roboto Fallback", 22 | }, 23 | }, 24 | }, 25 | }, 26 | }) 27 | 28 | const system = createSystem(defaultConfig, config) 29 | 30 | export function Provider(props: ColorModeProviderProps) { 31 | return ( 32 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /apps/front/src/components/ui/radio.tsx: -------------------------------------------------------------------------------- 1 | import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react" 2 | import * as React from "react" 3 | 4 | export interface RadioProps extends ChakraRadioGroup.ItemProps { 5 | rootRef?: React.Ref 6 | inputProps?: React.InputHTMLAttributes 7 | } 8 | 9 | export const Radio = React.forwardRef( 10 | function Radio(props, ref) { 11 | const { children, inputProps, rootRef, ...rest } = props 12 | return ( 13 | 14 | 15 | 16 | {children && ( 17 | {children} 18 | )} 19 | 20 | ) 21 | }, 22 | ) 23 | 24 | export const RadioGroup = ChakraRadioGroup.Root 25 | -------------------------------------------------------------------------------- /apps/front/src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/front/src/index.css -------------------------------------------------------------------------------- /apps/front/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react" 2 | import { createRoot } from "react-dom/client" 3 | import "./index.css" 4 | import App from "./App.tsx" 5 | import { Provider } from "./components/ui/provider.tsx" 6 | 7 | // biome-ignore lint/style/noNonNullAssertion: 8 | createRoot(document.getElementById("root")!).render( 9 | 10 | 11 | 12 | 13 | , 14 | ) 15 | -------------------------------------------------------------------------------- /apps/front/src/sections/Section.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Heading } from "@chakra-ui/react" 2 | 3 | export const Section = ({ children }: { children: React.ReactNode }) => { 4 | return ( 5 | 6 | {children} 7 | 8 | ) 9 | } 10 | 11 | export const SectionTitle = ({ children }: { children: React.ReactNode }) => { 12 | return ( 13 | 14 | {children} 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /apps/front/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/front/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | "strict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noUncheckedSideEffectImports": true, 20 | 21 | "paths": { 22 | "react": ["./node_modules/@types/react"] 23 | } 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /apps/front/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/front/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "isolatedModules": true, 11 | "moduleDetection": "force", 12 | "noEmit": true, 13 | "strict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noUncheckedSideEffectImports": true 18 | }, 19 | "include": ["vite.config.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/front/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }] 3 | } 4 | -------------------------------------------------------------------------------- /apps/front/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react" 2 | import { defineConfig } from "vite" 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [ 7 | // @ts-ignore 8 | react(), 9 | ], 10 | }) 11 | -------------------------------------------------------------------------------- /apps/tests/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | /test-results/ 26 | /playwright-report/ 27 | /blob-report/ 28 | /playwright/.cache/ 29 | -------------------------------------------------------------------------------- /apps/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + TS 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.120.0", 3 | "name": "prose-reader-tests", 4 | "type": "module", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "dev": "vite", 9 | "tsc": "tsc", 10 | "test": "npx playwright test", 11 | "test:ui": "npx playwright test --ui" 12 | }, 13 | "devDependencies": { 14 | "@playwright/test": "^1.48.2", 15 | "@prose-reader/core": "^1.120.0", 16 | "@prose-reader/enhancer-bookmarks": "^1.120.0", 17 | "@prose-reader/enhancer-gestures": "^1.120.0", 18 | "@prose-reader/enhancer-pdf": "^1.120.0", 19 | "@prose-reader/enhancer-search": "^1.120.0", 20 | "@prose-reader/streamer": "^1.120.0", 21 | "@vitejs/plugin-react": "^4.3.3", 22 | "pdfjs-dist": "^5.1.91", 23 | "react": "^19.0.0", 24 | "react-dom": "^19.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/tests/public/epubs/accessible_epub_3.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/public/epubs/accessible_epub_3.epub -------------------------------------------------------------------------------- /apps/tests/public/epubs/haruko-html-jpeg.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/public/epubs/haruko-html-jpeg.epub -------------------------------------------------------------------------------- /apps/tests/public/epubs/rendition-flow-webtoon.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/public/epubs/rendition-flow-webtoon.epub -------------------------------------------------------------------------------- /apps/tests/public/epubs/sample-3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/public/epubs/sample-3.pdf -------------------------------------------------------------------------------- /apps/tests/public/epubs/sample.cbz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/public/epubs/sample.cbz -------------------------------------------------------------------------------- /apps/tests/public/epubs/sous-le-vent.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/public/epubs/sous-le-vent.epub -------------------------------------------------------------------------------- /apps/tests/public/images/torinome.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/public/images/torinome.jpeg -------------------------------------------------------------------------------- /apps/tests/tests/bookmarks/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | foo 8 | 9 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/tests/tests/bookmarks/pdf.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test("should be able to mark bookmarks on pdf (no nodes)", async ({ page }) => { 4 | await page.setViewportSize({ 5 | width: 300, 6 | height: 400, 7 | }) 8 | 9 | await page.goto("http://localhost:3333/tests/bookmarks/index.html") 10 | 11 | // wait for first item to be ready 12 | await page.waitForSelector(".prose-spineItem-ready") 13 | 14 | await page.keyboard.press("ArrowRight") 15 | 16 | await page.waitForTimeout(200) 17 | 18 | const markButton = page.locator("#mark") 19 | 20 | // red 21 | await expect(markButton).toHaveCSS("background-color", "rgb(255, 0, 0)") 22 | 23 | await markButton.click() 24 | 25 | await page.waitForTimeout(200) 26 | 27 | // green 28 | await expect(markButton).toHaveCSS("background-color", "rgb(0, 128, 0)") 29 | 30 | await page.keyboard.press("ArrowLeft") 31 | 32 | await page.waitForTimeout(200) 33 | 34 | // red 35 | await expect(markButton).toHaveCSS("background-color", "rgb(255, 0, 0)") 36 | }) 37 | -------------------------------------------------------------------------------- /apps/tests/tests/layout/webtoon/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /apps/tests/tests/layout/webtoon/spine-item-layout.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | import { locateSpineItems } from "../../utils" 3 | 4 | test("first spine item should have correct width and height", async ({ 5 | page, 6 | }) => { 7 | await page.setViewportSize({ 8 | width: 300, 9 | height: 400, 10 | }) 11 | 12 | await page.goto("http://localhost:3333/tests/layout/webtoon/index.html") 13 | 14 | const spineItems = await locateSpineItems({ 15 | page, 16 | indexes: [0], 17 | }) 18 | // biome-ignore lint/style/noNonNullAssertion: 19 | const spineItem1 = spineItems[0]! 20 | 21 | await spineItem1.waitFor({ state: "visible" }) 22 | 23 | const boundingBox = await spineItem1.boundingBox() 24 | expect(boundingBox).not.toBeNull() 25 | expect(boundingBox?.width).toBe(300) 26 | expect(boundingBox?.height).toBe(1306) 27 | }) 28 | -------------------------------------------------------------------------------- /apps/tests/tests/navigation/manual/comics/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | foo 8 | 9 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/tests/tests/navigation/manual/rtl-haruko/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | foo 8 | 9 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/tests/tests/navigation/restoration/epub/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | foo 8 | 9 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/tests/tests/navigation/restoration/rtl-haruko/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | foo 8 | 9 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/tests/tests/navigation/scrolling/pdf/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | foo 8 | 9 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-navigate-to-second-page-and-back-to-first-page-1-chromium-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-navigate-to-second-page-and-back-to-first-page-1-chromium-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-navigate-to-second-page-and-back-to-first-page-1-firefox-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-navigate-to-second-page-and-back-to-first-page-1-firefox-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-navigate-to-second-page-and-back-to-first-page-1-webkit-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-navigate-to-second-page-and-back-to-first-page-1-webkit-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-navigate-to-second-page-and-back-to-first-page-2-chromium-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-navigate-to-second-page-and-back-to-first-page-2-chromium-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-navigate-to-second-page-and-back-to-first-page-2-firefox-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-navigate-to-second-page-and-back-to-first-page-2-firefox-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-navigate-to-second-page-and-back-to-first-page-2-webkit-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-navigate-to-second-page-and-back-to-first-page-2-webkit-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-restore-to-first-page-with-CFI-1-chromium-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-restore-to-first-page-with-CFI-1-chromium-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-restore-to-first-page-with-CFI-1-firefox-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-restore-to-first-page-with-CFI-1-firefox-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-restore-to-first-page-with-CFI-1-webkit-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-restore-to-first-page-with-CFI-1-webkit-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-restore-to-second-page-with-CFI-1-chromium-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-restore-to-second-page-with-CFI-1-chromium-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-restore-to-second-page-with-CFI-1-firefox-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-restore-to-second-page-with-CFI-1-firefox-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-restore-to-second-page-with-CFI-1-webkit-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/navigation/scrolling/pdf/pdf.spec.ts-snapshots/should-restore-to-second-page-with-CFI-1-webkit-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/pagination/progression/no-weight-manifest/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /apps/tests/tests/pagination/progression/no-weight-manifest/progression.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Given undefined progression weight", () => { 4 | test("should set progression based on number of items", async ({ page }) => { 5 | await page.setViewportSize({ 6 | width: 300, 7 | height: 400, 8 | }) 9 | 10 | await page.goto( 11 | "http://localhost:3333/tests/pagination/progression/no-weight-manifest/index.html", 12 | ) 13 | 14 | await expect(page.locator('text="progression: 0.5"')).toBeVisible() 15 | 16 | await page.keyboard.press("ArrowRight") 17 | 18 | await expect(page.locator('text="progression: 1"')).toBeVisible() 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /apps/tests/tests/prepaginated/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /apps/tests/tests/prepaginated/layout-spread.spec.ts-snapshots/page-spread-right-chromium-darwin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/prepaginated/layout-spread.spec.ts-snapshots/page-spread-right-chromium-darwin.jpg -------------------------------------------------------------------------------- /apps/tests/tests/prepaginated/layout-spread.spec.ts-snapshots/page-spread-right-firefox-darwin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/prepaginated/layout-spread.spec.ts-snapshots/page-spread-right-firefox-darwin.jpg -------------------------------------------------------------------------------- /apps/tests/tests/prepaginated/layout-spread.spec.ts-snapshots/page-spread-right-webkit-darwin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/prepaginated/layout-spread.spec.ts-snapshots/page-spread-right-webkit-darwin.jpg -------------------------------------------------------------------------------- /apps/tests/tests/prepaginated/layout-spread.spec.ts-snapshots/right-navigation-layout-chromium-darwin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/prepaginated/layout-spread.spec.ts-snapshots/right-navigation-layout-chromium-darwin.jpg -------------------------------------------------------------------------------- /apps/tests/tests/prepaginated/layout-spread.spec.ts-snapshots/right-navigation-layout-firefox-darwin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/prepaginated/layout-spread.spec.ts-snapshots/right-navigation-layout-firefox-darwin.jpg -------------------------------------------------------------------------------- /apps/tests/tests/prepaginated/layout-spread.spec.ts-snapshots/right-navigation-layout-webkit-darwin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/prepaginated/layout-spread.spec.ts-snapshots/right-navigation-layout-webkit-darwin.jpg -------------------------------------------------------------------------------- /apps/tests/tests/text/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | foo 8 | 9 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /apps/tests/tests/text/text.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test("should display basic text", async ({ page }) => { 4 | await page.setViewportSize({ 5 | width: 300, 6 | height: 400, 7 | }) 8 | 9 | await page.goto("http://localhost:3333/tests/text/index.html") 10 | 11 | // wait for first item to be ready 12 | await page.waitForSelector(".prose-spineItem-ready") 13 | 14 | expect(await page.screenshot()).toMatchSnapshot(`text.png`) 15 | }) 16 | -------------------------------------------------------------------------------- /apps/tests/tests/text/text.spec.ts-snapshots/text-chromium-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/text/text.spec.ts-snapshots/text-chromium-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/text/text.spec.ts-snapshots/text-firefox-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/text/text.spec.ts-snapshots/text-firefox-darwin.png -------------------------------------------------------------------------------- /apps/tests/tests/text/text.spec.ts-snapshots/text-webkit-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/apps/tests/tests/text/text.spec.ts-snapshots/text-webkit-darwin.png -------------------------------------------------------------------------------- /apps/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.web.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | 6 | "target": "ES2020", 7 | "useDefineForClassFields": true, 8 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 9 | "module": "ESNext", 10 | "skipLibCheck": true, 11 | 12 | /* Bundler mode */ 13 | "moduleResolution": "Bundler", 14 | "allowImportingTsExtensions": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/tests/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /apps/tests/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react" 2 | import { defineConfig } from "vite" 3 | 4 | export default defineConfig(() => { 5 | return { 6 | build: { 7 | minify: false, 8 | }, 9 | optimizeDeps: { 10 | esbuildOptions: { 11 | // Node.js global to browser globalThis 12 | // fix sax on browser 13 | define: { 14 | global: "globalThis", 15 | }, 16 | }, 17 | }, 18 | server: { 19 | port: 3333, 20 | }, 21 | plugins: [react()], 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "npm", 3 | "version": "1.219.0", 4 | "command": { 5 | "publish": { 6 | "graphType": "all" 7 | } 8 | }, 9 | "$schema": "node_modules/lerna/schemas/lerna-schema.json" 10 | } 11 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "targetDefaults": { 3 | "build": { 4 | "dependsOn": ["^build"], 5 | "outputs": ["{projectRoot}/dist"], 6 | "cache": true 7 | }, 8 | "lint": { 9 | "cache": true 10 | } 11 | }, 12 | "namedInputs": { 13 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 14 | "sharedGlobals": [], 15 | "production": ["default"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/cfi/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/cfi/README.md: -------------------------------------------------------------------------------- 1 | Visit the doc at https://doc.prose-reader.com/cfi -------------------------------------------------------------------------------- /packages/cfi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prose-reader/cfi", 3 | "version": "1.219.0", 4 | "type": "module", 5 | "files": ["/dist"], 6 | "main": "./dist/index.umd.cjs", 7 | "module": "./dist/index.js", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/index.js", 11 | "require": "./dist/index.umd.cjs" 12 | } 13 | }, 14 | "scripts": { 15 | "dev": "vite", 16 | "start": "vite build --watch --mode development", 17 | "build": "tsc && vite build", 18 | "preview": "vite preview", 19 | "test": "vitest run" 20 | }, 21 | "devDependencies": { 22 | "jsdom": "^26.1.0", 23 | "typescript": "~5.8.3", 24 | "vite": "^6.3.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/cfi/src/index.ts: -------------------------------------------------------------------------------- 1 | export { parse, type ParsedCfi, type CfiPart } from "./parse" 2 | export { resolve } from "./resolve" 3 | export { generate } from "./generate" 4 | export { compare } from "./compare" 5 | export { resolveExtensions } from "./resolve" 6 | export { serialize } from "./serialize" 7 | export { isIndirectionOnly, isParsedCfiRange } from "./utils" 8 | -------------------------------------------------------------------------------- /packages/cfi/src/parse.indirection.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest" 2 | import { parse } from "./parse" 3 | 4 | it("should parse a CFI with indirection", () => { 5 | const cfi = "epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2:3)" 6 | const parsed = parse(cfi) 7 | 8 | expect(parsed).toEqual([ 9 | [{ index: 6 }, { index: 4, id: "chap01ref" }], 10 | [ 11 | { index: 4, id: "body01" }, 12 | { index: 10, id: "para05" }, 13 | { index: 2, offset: 3 }, 14 | ], 15 | ]) 16 | }) 17 | 18 | it("should parse a CFI with indirection only", () => { 19 | const cfi = "epubcfi(/6/4[chap01ref]!)" 20 | const parsed = parse(cfi) 21 | 22 | expect(parsed).toEqual([[{ index: 6 }, { index: 4, id: "chap01ref" }], []]) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/cfi/src/tests/test2.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises" 2 | import { describe, expect, it } from "vitest" 3 | import { generate } from "../generate" 4 | import { resolve } from "../resolve" 5 | 6 | describe("test2.xhtml", () => { 7 | it("should correctly target img node", async () => { 8 | const domParser = new DOMParser() 9 | const test1Xhtml = await fs.readFile(`${__dirname}/test2.xhtml`, "utf-8") 10 | const doc = domParser.parseFromString(test1Xhtml, "application/xhtml+xml") 11 | 12 | const imageNode = doc.body.childNodes[1] 13 | if (!imageNode) { 14 | throw new Error("Node not found") 15 | } 16 | 17 | const cfi = generate(imageNode) 18 | 19 | // this ends with /1 to indicate that it is the first child of the element 20 | expect(cfi).toBe("epubcfi(/4/2)") 21 | 22 | const resolvedText = resolve(cfi, doc) 23 | 24 | expect(resolvedText.node).toBe(imageNode) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/cfi/src/tests/test2.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | ハルコさんの彼氏 7 | 8 | 9 | 10 | お手洗い! でも驚いたわ 何が? 奥手だと思ってたけどハルコ結構やるじゃん やるじゃんって何が? まーたまたー 今日の最高スペックのオトコじゃないあれ 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/cfi/src/tests/test4.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises" 2 | import { describe, expect, it } from "vitest" 3 | import { resolve } from "../resolve" 4 | 5 | describe("Given a document with whitespace nodes", () => { 6 | it("should correctly target a node in a document which contains whitespace child nodes", async () => { 7 | const domParser = new DOMParser() 8 | const test1Xhtml = await fs.readFile(`${__dirname}/test4.xhtml`, "utf-8") 9 | const doc = domParser.parseFromString(test1Xhtml, "application/xhtml+xml") 10 | 11 | const cfi = "epubcfi(/4/4[toc]/4/8/4/2/4/2/2/1)" 12 | 13 | const resolved = resolve(cfi, doc) 14 | 15 | if (!(resolved.node instanceof Node)) throw new Error("Invalid node") 16 | 17 | expect(resolved.node.textContent).not.toBe(`\n\t\t\t`) 18 | expect(resolved.node?.textContent).toBe("Timed Tracks") 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/cfi/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/cfi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.web.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/cfi/vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import { resolve } from "node:path" 3 | import { defineConfig } from "vite" 4 | import dts from "vite-plugin-dts" 5 | import { name } from "./package.json" 6 | 7 | const libName = name.replace(`@`, ``).replace(`/`, `-`) 8 | 9 | export default defineConfig(({ mode }) => { 10 | return { 11 | build: { 12 | lib: { 13 | // Could also be a dictionary or array of multiple entry points 14 | entry: resolve(__dirname, `src/index.ts`), 15 | name: libName, 16 | fileName: `index`, 17 | }, 18 | emptyOutDir: mode !== "development", 19 | sourcemap: true, 20 | }, 21 | plugins: [ 22 | dts({ 23 | entryRoot: "src", 24 | }), 25 | ], 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /packages/cfi/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig(() => ({ 4 | test: { 5 | environment: "jsdom", 6 | coverage: { 7 | reportsDirectory: `./.test/coverage`, 8 | }, 9 | }, 10 | })) 11 | -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .test/coverage -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prose-reader/core", 3 | "version": "1.219.0", 4 | "type": "module", 5 | "main": "./dist/index.umd.cjs", 6 | "module": "./dist/index.js", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/index.js", 10 | "require": "./dist/index.umd.cjs" 11 | } 12 | }, 13 | "types": "./dist/index.d.ts", 14 | "license": "MIT", 15 | "files": ["/dist"], 16 | "scripts": { 17 | "start": "vite build --watch --mode development", 18 | "build": "tsc && vite build", 19 | "test": "vitest run --coverage", 20 | "test:watch": "vitest watch", 21 | "tsc": "tsc" 22 | }, 23 | "dependencies": { 24 | "@prose-reader/cfi": "^1.219.0", 25 | "@prose-reader/shared": "^1.219.0" 26 | }, 27 | "peerDependencies": { 28 | "rxjs": "*" 29 | }, 30 | "devDependencies": { 31 | "happy-dom": "^17.1.0" 32 | }, 33 | "madge": { 34 | "detectiveOptions": { 35 | "ts": { 36 | "skipTypeImports": true 37 | } 38 | } 39 | }, 40 | "gitHead": "4601e14dcacf50b2295cb343582a7ef2c7e1eedc" 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/cfi/generate.test.ts: -------------------------------------------------------------------------------- 1 | import type { Manifest } from "@prose-reader/shared" 2 | import { describe, expect, it } from "vitest" 3 | import { generateRootCfi } from "./generate" 4 | 5 | describe("generateRootCfi", () => { 6 | it("should generate a root cfi", () => { 7 | const cfi = generateRootCfi({ 8 | id: "item1", 9 | href: "item1.html", 10 | index: 1, 11 | } as unknown as Manifest["spineItems"][number]) 12 | 13 | expect(cfi).toBe("epubcfi(/6/4[item1]!)") 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/core/src/cfi/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./generate" 2 | export * from "./parse" 3 | export * from "./resolve" 4 | -------------------------------------------------------------------------------- /packages/core/src/cfi/parse.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { isRootCfi } from "../cfi/parse" 3 | 4 | describe("isRootCfi", () => { 5 | it("should return false for a non-root CFI", () => { 6 | expect(isRootCfi("epubcfi(/4[body01]/10[para05]/2:3[Hello,World])")).toBe( 7 | false, 8 | ) 9 | expect(isRootCfi("epubcfi(/6/2!/4/10/2)")).toBe(false) 10 | }) 11 | 12 | it("should return true for a root CFI", () => { 13 | expect(isRootCfi("epubcfi(/6/2!)")).toBe(true) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/core/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const PROSE_READER_NAMESPACE = `@prose-reader/core` 2 | export const VIEWPORT_ADJUSTMENT_THROTTLE = 0 3 | export const PAGINATION_UPDATE_AFTER_VIEWPORT_ADJUSTMENT_DEBOUNCE = 200 4 | export const ITEM_EXTENSION_VALID_FOR_FRAME_SRC = [`.xhtml`, `.html`, `.htm`] 5 | export const HTML_PREFIX = `prose-reader` 6 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/firefox.ts: -------------------------------------------------------------------------------- 1 | import type { EnhancerOutput, RootEnhancer } from "./types/enhancer" 2 | 3 | export const firefoxEnhancer = 4 | >( 5 | next: (options: InheritOptions) => InheritOutput, 6 | ) => 7 | (options: InheritOptions): InheritOutput => { 8 | const reader = next(options) 9 | 10 | // add all normalization 11 | 12 | return reader 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/fonts/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fonts" 2 | export * from "./types" 3 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/fonts/types.ts: -------------------------------------------------------------------------------- 1 | const FONT_WEIGHT = [100, 200, 300, 400, 500, 600, 700, 800, 900] as const 2 | const FONT_JUSTIFICATION = [`center`, `left`, `right`, `justify`] as const 3 | 4 | export type EnhancerFontsInputSettings = { 5 | /** 6 | * @description 7 | * Scale the font size. 1 means use default publisher/browser font size, 2 means 200% 8 | * 0.5 50%, etc 9 | */ 10 | fontScale: number 11 | /** 12 | * @description 13 | * Set the line height of the text. The default value is 1 14 | */ 15 | lineHeight: number | `publisher` 16 | /** 17 | * @description 18 | * Set font weight of text 19 | */ 20 | fontWeight: (typeof FONT_WEIGHT)[number] | `publisher` 21 | /** 22 | * @description 23 | * Set text align justification 24 | */ 25 | fontJustification: (typeof FONT_JUSTIFICATION)[number] | `publisher` 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/html/links.ts: -------------------------------------------------------------------------------- 1 | import { NEVER, fromEvent, merge, share, switchMap, tap } from "rxjs" 2 | import type { Reader } from "../../reader" 3 | 4 | export const handleLinks = (reader: Reader) => { 5 | return reader.spine.spineItemsManager.items$.pipe( 6 | switchMap((items) => 7 | merge( 8 | ...items.map((item) => { 9 | return item.loaded$.pipe( 10 | switchMap(() => { 11 | const frame = item.renderer.getDocumentFrame() 12 | 13 | if (!frame || !frame?.contentDocument) return NEVER 14 | 15 | const anchorElements = Array.from( 16 | frame.contentDocument.querySelectorAll(`a`), 17 | ) 18 | 19 | const events$ = anchorElements.map((element) => 20 | fromEvent(element, `click`), 21 | ) 22 | 23 | return merge(...events$) 24 | }), 25 | ) 26 | }), 27 | ), 28 | ), 29 | tap((event) => { 30 | event.preventDefault() 31 | }), 32 | share(), 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/html/renderer/createFrameElement.ts: -------------------------------------------------------------------------------- 1 | export const createFrameElement = () => { 2 | // we force undefined because otherwise the load method will believe it's defined after this call but the code is async and 3 | // the iframe could be undefined later 4 | const frame = document.createElement(`iframe`) 5 | frame.frameBorder = `no` 6 | frame.tabIndex = 0 7 | frame.setAttribute( 8 | `sandbox`, 9 | ` 10 | allow-same-origin 11 | allow-scripts 12 | allow-top-navigation-to-custom-protocols 13 | `, 14 | ) 15 | frame.style.cssText = ` 16 | overflow: hidden; 17 | background-color: transparent; 18 | border: 0px none transparent; 19 | padding: 0px; 20 | ` 21 | 22 | frame.setAttribute(`role`, `main`) 23 | 24 | return frame 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/layout/types.ts: -------------------------------------------------------------------------------- 1 | export type EnhancerLayoutInputSettings = { 2 | pageHorizontalMargin: number 3 | pageVerticalMargin: number 4 | /** 5 | * Can be used to let the reader automatically resize. 6 | * `container`: observe and resize the reader whenever the container resize. 7 | * `false`: do not automatically resize. 8 | */ 9 | layoutAutoResize: `container` | false 10 | /** 11 | * Whether to use a CSS transition when spine item is ready. 12 | */ 13 | layoutLayerTransition: boolean 14 | viewportMode: `normal` | `thumbnails` 15 | } 16 | 17 | export type OutputSettings = EnhancerLayoutInputSettings 18 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/layout/viewportMode.ts: -------------------------------------------------------------------------------- 1 | import { type Observable, tap } from "rxjs" 2 | import type { Reader } from "../../reader" 3 | 4 | export const createViewportModeHandler = ( 5 | reader: Reader, 6 | viewportMode$: Observable<`normal` | `thumbnails`>, 7 | ) => { 8 | return viewportMode$.pipe( 9 | tap((viewportMode) => { 10 | reader.viewport.value.element.style.transition = `transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275)` 11 | 12 | if (reader.settings.values.computedPageTurnMode === "scrollable") { 13 | reader.viewport.value.element.style.transformOrigin = `top` 14 | } else { 15 | reader.viewport.value.element.style.transformOrigin = `center` 16 | } 17 | 18 | if (viewportMode === `thumbnails`) { 19 | reader.viewport.value.element.style.transform = `scale(0.5)` 20 | } else { 21 | reader.viewport.value.element.style.transform = `scale(1)` 22 | } 23 | reader.layout() 24 | }), 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/loading/constants.ts: -------------------------------------------------------------------------------- 1 | import { HTML_PREFIX as HTML_PREFIX_CORE } from "../../constants" 2 | 3 | export const HTML_PREFIX = `${HTML_PREFIX_CORE}-enhancer-loading` 4 | export const CONTAINER_HTML_PREFIX = `${HTML_PREFIX}-container` 5 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/navigation/links.ts: -------------------------------------------------------------------------------- 1 | import { tap } from "rxjs" 2 | import type { Reader } from "../../reader" 3 | import { isHtmlTagElement } from "../../utils/dom" 4 | import type { HtmlEnhancerOutput } from "../html/enhancer" 5 | import type { ManualNavigator } from "./navigators/manualNavigator" 6 | 7 | export const handleLinksNavigation = ( 8 | reader: Reader & HtmlEnhancerOutput, 9 | manualNavigator: ManualNavigator, 10 | ) => { 11 | return reader.links$.pipe( 12 | tap((event) => { 13 | if (!isHtmlTagElement(event.target, "a") || event.type !== "click") return 14 | 15 | const hrefUrl = new URL(event.target.href) 16 | const hrefWithoutAnchor = `${hrefUrl.origin}${hrefUrl.pathname}` 17 | 18 | // internal link, we can handle 19 | const hasExistingSpineItem = reader.context.manifest?.spineItems.some( 20 | (item) => item.href === hrefWithoutAnchor, 21 | ) 22 | 23 | if (hasExistingSpineItem) { 24 | manualNavigator.goToUrl(hrefUrl) 25 | } 26 | }), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/navigation/report.ts: -------------------------------------------------------------------------------- 1 | import { Report } from "../../report" 2 | 3 | export const navigationReport = Report.namespace(`navigation`) 4 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/navigation/throttleLock.ts: -------------------------------------------------------------------------------- 1 | import { 2 | animationFrameScheduler, 3 | finalize, 4 | type Observable, 5 | tap, 6 | throttleTime, 7 | } from "rxjs" 8 | import type { Reader } from "../../reader" 9 | 10 | export const throttleLock = 11 | ({ reader, duration }: { reader: Reader; duration: number }) => 12 | (stream: Observable) => { 13 | let unlockFn: (() => void) | undefined = undefined 14 | const unlock = () => { 15 | unlockFn?.() 16 | unlockFn = undefined 17 | } 18 | 19 | return stream.pipe( 20 | tap(() => { 21 | if (!unlockFn) { 22 | unlockFn = reader?.navigation.lock() 23 | } 24 | }), 25 | throttleTime(duration, animationFrameScheduler, { 26 | trailing: true, 27 | leading: true, 28 | }), 29 | tap(unlock), 30 | finalize(unlock), 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/types/enhancer.ts: -------------------------------------------------------------------------------- 1 | import type { CreateReaderParameters, ReaderInternal } from "../../reader" 2 | 3 | // biome-ignore lint/suspicious/noExplicitAny: 4 | export type EnhancerOutput any> = ReturnType< 5 | ReturnType 6 | > 7 | // biome-ignore lint/suspicious/noExplicitAny: 8 | export type EnhancerOptions any> = 9 | Parameters>[0] 10 | 11 | export type RootEnhancer< 12 | Options extends CreateReaderParameters = CreateReaderParameters, 13 | Reader extends ReaderInternal = ReaderInternal, 14 | > = (next: (options: Options) => Reader) => (options: Options) => Reader 15 | 16 | export const rootEnhancer = 17 | ( 18 | next: (options: Options) => Reader, 19 | ) => 20 | (options: Options): Reader => { 21 | const reader = next(options) 22 | 23 | return reader 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/utils.ts: -------------------------------------------------------------------------------- 1 | import { isHtmlElement } from "../utils/dom" 2 | import type { EnhancerOutput, RootEnhancer } from "./types/enhancer" 3 | 4 | export const utilsEnhancer = 5 | >( 6 | next: (options: InheritOptions) => InheritOutput, 7 | ) => 8 | ( 9 | options: InheritOptions, 10 | ): InheritOutput & { 11 | utils: { 12 | isOrIsWithinValidLink: (target: Event[`target`]) => boolean 13 | } 14 | } => { 15 | const reader = next(options) 16 | 17 | const isOrIsWithinValidLink = (target: Event[`target`]) => { 18 | if (isHtmlElement(target)) { 19 | const link = target.nodeName === `a` ? target : target.closest(`a`) 20 | if (link?.getAttribute(`href`)) { 21 | return true 22 | } 23 | } 24 | 25 | return false 26 | } 27 | 28 | return { 29 | ...reader, 30 | utils: { 31 | isOrIsWithinValidLink, 32 | }, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/zoom/Zoomer.ts: -------------------------------------------------------------------------------- 1 | import type { BehaviorSubject } from "rxjs" 2 | import type { Reader } from "../../reader" 3 | 4 | export abstract class Zoomer { 5 | constructor(protected reader: Reader) {} 6 | 7 | public abstract enter(element?: HTMLImageElement): void 8 | public abstract exit(): void 9 | public abstract moveAt(position: { x: number; y: number }): void 10 | public abstract scaleAt(scale: number): void 11 | 12 | public abstract element: HTMLDivElement | undefined 13 | public abstract isZooming$: BehaviorSubject 14 | public abstract currentScale: number 15 | public abstract currentPosition: { x: number; y: number } 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/enhancers/zoom/types.ts: -------------------------------------------------------------------------------- 1 | import type { Observable } from "rxjs" 2 | 3 | export type ZoomEnhancerOutput = { 4 | zoom: { 5 | enter: (imgElement?: HTMLImageElement) => void 6 | exit: () => void 7 | moveAt: (position: { x: number; y: number }) => void 8 | scaleAt: (scale: number) => void 9 | currentScale: number 10 | currentPosition: { x: number; y: number } 11 | zoomContainerElement: HTMLDivElement | undefined 12 | isZooming$: Observable 13 | isZooming: boolean 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/manifest/isFullyPrePaginated.ts: -------------------------------------------------------------------------------- 1 | import type { Manifest } from "@prose-reader/shared" 2 | 3 | export const isFullyPrePaginated = (manifest?: Manifest) => 4 | manifest?.renditionLayout === "pre-paginated" || 5 | manifest?.spineItems.every((item) => item.renditionLayout === "pre-paginated") 6 | -------------------------------------------------------------------------------- /packages/core/src/navigation/Locker.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject, distinctUntilChanged, map } from "rxjs" 2 | 3 | export class Locker { 4 | protected isLockedSubject = new BehaviorSubject(0) 5 | 6 | public isLocked$ = this.isLockedSubject.pipe( 7 | map((locked) => !!locked), 8 | distinctUntilChanged(), 9 | ) 10 | 11 | public lock() { 12 | let isCalled = false 13 | this.isLockedSubject.next(this.isLockedSubject.getValue() + 1) 14 | 15 | return () => { 16 | if (isCalled) return 17 | 18 | isCalled = true 19 | 20 | this.isLockedSubject.next(this.isLockedSubject.getValue() - 1) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/navigation/consolidation/mapUserNavigationToInternal.ts: -------------------------------------------------------------------------------- 1 | import { type Observable, map } from "rxjs" 2 | import type { 3 | InternalNavigationEntry, 4 | InternalNavigationInput, 5 | UserNavigationEntry, 6 | } from "../types" 7 | 8 | export const mapUserNavigationToInternal = ( 9 | stream: Observable<[UserNavigationEntry, InternalNavigationEntry]>, 10 | ): Observable<{ 11 | navigation: InternalNavigationInput 12 | previousNavigation: InternalNavigationEntry 13 | }> => { 14 | return stream.pipe( 15 | map(([userNavigation, previousNavigation]) => { 16 | const navigation: InternalNavigationInput = { 17 | type: "api", 18 | meta: { 19 | triggeredBy: "user", 20 | }, 21 | id: Symbol(), 22 | animation: "turn", 23 | ...userNavigation, 24 | } 25 | 26 | return { 27 | previousNavigation, 28 | navigation, 29 | } 30 | }), 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/navigation/consolidation/withCfiPosition.ts: -------------------------------------------------------------------------------- 1 | import { type Observable, map } from "rxjs" 2 | import type { NavigationResolver } from "../resolvers/NavigationResolver" 3 | import type { InternalNavigationInput } from "../types" 4 | 5 | type Navigation = { 6 | navigation: InternalNavigationInput 7 | } 8 | 9 | export const withCfiPosition = 10 | ({ navigationResolver }: { navigationResolver: NavigationResolver }) => 11 | (stream: Observable): Observable => { 12 | return stream.pipe( 13 | map((params) => { 14 | if (params.navigation.cfi) { 15 | const position = navigationResolver.getNavigationForCfi( 16 | params.navigation.cfi, 17 | ) 18 | 19 | if (position) { 20 | return { 21 | ...params, 22 | navigation: { 23 | ...params.navigation, 24 | position, 25 | }, 26 | } as N 27 | } 28 | } 29 | 30 | return params 31 | }), 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/navigation/consolidation/withPaginationInfo.ts: -------------------------------------------------------------------------------- 1 | import { type Observable, map } from "rxjs" 2 | import type { PaginationInfo } from "../../pagination" 3 | import type { InternalNavigationEntry } from "../types" 4 | 5 | type Navigation = { 6 | navigation: InternalNavigationEntry 7 | pagination: PaginationInfo 8 | } 9 | 10 | export const withPaginationInfo = 11 | () => 12 | (stream: Observable): Observable => { 13 | return stream.pipe( 14 | map(({ navigation, pagination, ...rest }) => { 15 | return { 16 | navigation: { 17 | ...navigation, 18 | paginationBeginCfi: pagination.beginCfi, 19 | }, 20 | ...rest, 21 | } as N 22 | }), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/navigation/controllers/positions.ts: -------------------------------------------------------------------------------- 1 | import { SpinePosition } from "../../spine/types" 2 | 3 | /** 4 | * LTR uses positive spine position and translate to negative translation. 5 | * Works both way for RTL. 6 | * @returns 7 | */ 8 | export const spinePositionToTranslation = (position: SpinePosition) => { 9 | return { 10 | x: -position.x, 11 | y: -position.y, 12 | } 13 | } 14 | 15 | export const translationToSpinePosition = ( 16 | translation: { x: number; y: number } | DOMMatrix, 17 | ): SpinePosition => { 18 | if (translation instanceof DOMMatrix) { 19 | return new SpinePosition({ 20 | x: -translation.e, 21 | y: -translation.f, 22 | }) 23 | } 24 | 25 | return new SpinePosition({ 26 | x: -translation.x, 27 | y: -translation.y, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/navigation/resolvers/getAdjustedPositionForSpread.ts: -------------------------------------------------------------------------------- 1 | import { SpinePosition } from "../../spine/types" 2 | import type { DeprecatedViewportPosition } from "../types" 3 | 4 | export const getAdjustedPositionForSpread = ({ 5 | position: { x, y }, 6 | pageSizeWidth, 7 | visibleAreaRectWidth, 8 | }: { 9 | position: DeprecatedViewportPosition | SpinePosition 10 | pageSizeWidth: number 11 | visibleAreaRectWidth: number 12 | }): DeprecatedViewportPosition => { 13 | const isOffsetNotAtEdge = x % visibleAreaRectWidth !== 0 14 | const correctedX = isOffsetNotAtEdge ? x - pageSizeWidth : x 15 | 16 | return new SpinePosition({ x: correctedX, y }) 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/pagination/Pagination.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../context/Context" 2 | import type { SpineItemsManager } from "../spine/SpineItemsManager" 3 | import { ReactiveEntity } from "../utils/ReactiveEntity" 4 | import type { PaginationInfo } from "./types" 5 | 6 | export class Pagination extends ReactiveEntity { 7 | constructor( 8 | protected context: Context, 9 | protected spineItemsManager: SpineItemsManager, 10 | ) { 11 | super({ 12 | beginPageIndexInSpineItem: undefined, 13 | beginNumberOfPagesInSpineItem: 0, 14 | beginCfi: undefined, 15 | beginSpineItemIndex: undefined, 16 | endPageIndexInSpineItem: undefined, 17 | endNumberOfPagesInSpineItem: 0, 18 | endCfi: undefined, 19 | endSpineItemIndex: undefined, 20 | navigationId: undefined, 21 | }) 22 | } 23 | 24 | public update(pagination: Partial) { 25 | this.mergeCompare(pagination) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/pagination/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Pagination" 2 | export * from "./types" 3 | -------------------------------------------------------------------------------- /packages/core/src/pagination/types.ts: -------------------------------------------------------------------------------- 1 | export type PaginationInfo = { 2 | beginPageIndexInSpineItem: number | undefined 3 | beginNumberOfPagesInSpineItem: number 4 | beginCfi: string | undefined 5 | beginSpineItemIndex: number | undefined 6 | endPageIndexInSpineItem: number | undefined 7 | endNumberOfPagesInSpineItem: number 8 | endCfi: string | undefined 9 | endSpineItemIndex: number | undefined 10 | navigationId?: symbol 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/report.ts: -------------------------------------------------------------------------------- 1 | const ROOT_NAMESPACE = `@prose-reader/core` 2 | 3 | import { Report as SharedReport } from "@prose-reader/shared" 4 | 5 | export const Report = SharedReport.namespace(ROOT_NAMESPACE) 6 | -------------------------------------------------------------------------------- /packages/core/src/settings/SettingsInterface.ts: -------------------------------------------------------------------------------- 1 | import type { Observable } from "rxjs" 2 | 3 | export interface SettingsInterface< 4 | InputSettings, 5 | OutputSettings = Record, 6 | > { 7 | _outputSettings?: OutputSettings 8 | _inputSettings?: InputSettings 9 | 10 | _prepareUpdate(settings: Partial): { 11 | hasChanged: boolean 12 | commit: () => OutputSettings 13 | } 14 | 15 | update(settings: Partial): void 16 | 17 | values$: Observable 18 | 19 | values: OutputSettings 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/spine/locator/getSpinePositionFromSpineItemPageIndex.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context/Context" 2 | import { getSpineItemPositionFromPageIndex } from "../../spineItem/layout/getSpineItemPositionFromPageIndex" 3 | import { getSpinePositionFromSpineItemPosition } from "./getSpinePositionFromSpineItemPosition" 4 | 5 | export const getSpinePositionFromSpineItemPageIndex = ({ 6 | pageIndex, 7 | context, 8 | isUsingVerticalWriting, 9 | itemLayout, 10 | }: { 11 | pageIndex: number 12 | isUsingVerticalWriting: boolean 13 | context: Context 14 | itemLayout: { left: number; top: number; width: number; height: number } 15 | }) => { 16 | const spineItemPosition = getSpineItemPositionFromPageIndex({ 17 | pageIndex, 18 | context, 19 | isUsingVerticalWriting, 20 | itemLayout, 21 | }) 22 | 23 | return getSpinePositionFromSpineItemPosition({ 24 | itemLayout, 25 | spineItemPosition, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/spineItem/layout/getSpineItemNumberOfPages.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "../../context/Context" 2 | import type { ReaderSettingsManager } from "../../settings/ReaderSettingsManager" 3 | import { calculateNumberOfPagesForItem } from "../helpers" 4 | 5 | export const getSpineItemNumberOfPages = ({ 6 | itemHeight, 7 | itemWidth, 8 | isUsingVerticalWriting, 9 | settings, 10 | context, 11 | }: { 12 | itemWidth: number 13 | itemHeight: number 14 | isUsingVerticalWriting: boolean 15 | settings: ReaderSettingsManager 16 | context: Context 17 | }) => { 18 | const { pageTurnDirection, pageTurnMode } = settings.values 19 | 20 | if (pageTurnDirection === `vertical` && pageTurnMode === `scrollable`) { 21 | return 1 22 | } 23 | 24 | if (isUsingVerticalWriting || pageTurnDirection === `vertical`) { 25 | return calculateNumberOfPagesForItem( 26 | itemHeight, 27 | context.getPageSize().height, 28 | ) 29 | } 30 | 31 | return calculateNumberOfPagesForItem(itemWidth, context.getPageSize().width) 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/spineItem/renderer/DefaultRenderer.ts: -------------------------------------------------------------------------------- 1 | import { EMPTY, of } from "rxjs" 2 | import { DocumentRenderer } from "./DocumentRenderer" 3 | 4 | export class DefaultRenderer extends DocumentRenderer { 5 | onUnload() { 6 | return EMPTY 7 | } 8 | 9 | onCreateDocument() { 10 | return of(document.createElement("div")) 11 | } 12 | 13 | onLoadDocument() { 14 | return EMPTY 15 | } 16 | 17 | onLayout() { 18 | return of(undefined) 19 | } 20 | 21 | onRenderHeadless() { 22 | return EMPTY 23 | } 24 | 25 | getDocumentFrame() { 26 | return undefined 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/spineItem/resources/ResourceHandler.ts: -------------------------------------------------------------------------------- 1 | import type { Manifest } from "@prose-reader/shared" 2 | import type { ReaderSettingsManager } from "../../settings/ReaderSettingsManager" 3 | import { lastValueFrom, of } from "rxjs" 4 | 5 | const defaultGetResource = (item: Manifest["items"][0]) => new URL(item.href) 6 | 7 | export class ResourceHandler { 8 | constructor( 9 | protected item: Manifest["items"][number], 10 | protected settings: ReaderSettingsManager, 11 | ) {} 12 | 13 | public async getResource() { 14 | const resource = await lastValueFrom( 15 | this.settings.values.getResource?.(this.item) ?? of(undefined), 16 | ) 17 | 18 | return resource ?? defaultGetResource(this.item) 19 | } 20 | 21 | public async fetchResource() { 22 | const resource = await this.getResource() 23 | 24 | if (resource instanceof Response) return resource 25 | 26 | if (resource instanceof URL) return fetch(resource) 27 | 28 | return resource 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/spineItem/types.ts: -------------------------------------------------------------------------------- 1 | export class SpineItemPosition { 2 | public readonly x: number 3 | public readonly y: number 4 | public readonly __symbol = Symbol(`SpineItemPosition`) 5 | 6 | constructor(position: { x: number; y: number }) { 7 | this.x = position.x 8 | this.y = position.y 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/tests/utils.ts: -------------------------------------------------------------------------------- 1 | export const waitFor = async (timeout: number) => 2 | await new Promise((resolve) => setTimeout(resolve, timeout)) 3 | -------------------------------------------------------------------------------- /packages/core/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Manifest } from "@prose-reader/shared" 2 | 3 | export type { Manifest } 4 | -------------------------------------------------------------------------------- /packages/core/src/utils/DestroyableClass.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from "rxjs" 2 | 3 | export class DestroyableClass { 4 | protected isDestroyed = false 5 | private destroySubject = new Subject() 6 | 7 | protected destroy$ = this.destroySubject.asObservable() 8 | 9 | public destroy() { 10 | if (this.isDestroyed) return 11 | 12 | this.isDestroyed = true 13 | 14 | this.destroySubject.next() 15 | this.destroySubject.complete() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/utils/isDefined.ts: -------------------------------------------------------------------------------- 1 | export function isDefined( 2 | arg: T | null | undefined, 3 | ): arg is T extends null | undefined ? never : T { 4 | return arg !== null && arg !== undefined 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/utils/layout.ts: -------------------------------------------------------------------------------- 1 | export const getNewScaledOffset = ({ 2 | newScale, 3 | oldScale, 4 | screenSize, 5 | scrollOffset, 6 | }: { 7 | screenSize: number 8 | pageSize: number 9 | scrollOffset: number 10 | newScale: number 11 | oldScale: number 12 | }) => { 13 | const centerXPosition = 14 | (screenSize * newScale) / 2 - screenSize + screenSize / 2 15 | const oldCenterPosition = 16 | (screenSize * oldScale) / 2 - screenSize + screenSize / 2 17 | const scaleDifference = newScale / oldScale 18 | const realScrollOffset = scrollOffset - oldCenterPosition 19 | 20 | return Math.max(centerXPosition + realScrollOffset * scaleDifference, 0) 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/utils/manifest.ts: -------------------------------------------------------------------------------- 1 | import type { Manifest } from "@prose-reader/shared" 2 | 3 | /** 4 | * @todo strip out param url so that equality works better 5 | */ 6 | export const getCoverItem = (manifest: Manifest) => { 7 | const coverItem = manifest.guide?.find((item) => item.type === `cover`) 8 | 9 | return manifest.spineItems.findIndex((item) => { 10 | if (!coverItem?.href) return false 11 | 12 | return item.href.endsWith(coverItem.href) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/utils/objects.ts: -------------------------------------------------------------------------------- 1 | export { isShallowEqual } from "@prose-reader/shared" 2 | 3 | export const getBase64FromBlob = (data: Blob) => { 4 | const reader = new FileReader() 5 | 6 | return new Promise((resolve) => { 7 | reader.addEventListener( 8 | `load`, 9 | () => { 10 | resolve(reader.result as string) 11 | }, 12 | false, 13 | ) 14 | 15 | reader.readAsDataURL(data) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/viewport/translateSpinePositionToRelativeViewport.ts: -------------------------------------------------------------------------------- 1 | import type { DeprecatedViewportPosition } from "../navigation/types" 2 | import { type SpinePosition, UnsafeSpinePosition } from "../spine/types" 3 | import type { AbsoluteViewport, RelativeViewport } from "./types" 4 | 5 | export const translateSpinePositionToRelativeViewport = ( 6 | absolutePosition: DeprecatedViewportPosition | SpinePosition, 7 | absoluteViewport: AbsoluteViewport, 8 | relativeViewport: RelativeViewport | AbsoluteViewport, 9 | ): UnsafeSpinePosition => { 10 | // Calculate the offset needed to center the relative viewport within the absolute viewport 11 | const offsetX = (relativeViewport.width - absoluteViewport.width) / 2 12 | const offsetY = (relativeViewport.height - absoluteViewport.height) / 2 13 | 14 | return new UnsafeSpinePosition({ 15 | x: absolutePosition.x - offsetX, 16 | y: absolutePosition.y - offsetY, 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es6", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "strict": true, 9 | "moduleResolution": "Bundler", 10 | "allowSyntheticDefaultImports": true, 11 | "noUncheckedIndexedAccess": true, 12 | "declaration": false, 13 | "removeComments": true, 14 | "pretty": false, 15 | "stripInternal": true, 16 | "strictFunctionTypes": true, 17 | "strictBindCallApply": true, 18 | "noUnusedParameters": true, 19 | "noPropertyAccessFromIndexSignature": true, 20 | "sourceMap": true, 21 | "noEmit": true, 22 | "isolatedModules": true, 23 | "skipLibCheck": true 24 | }, 25 | "include": ["src/**/*"], 26 | "exclude": ["dist", "**/*.spec.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import { resolve } from "node:path" 3 | import externals from "rollup-plugin-node-externals" 4 | import { defineConfig } from "vite" 5 | import dts from "vite-plugin-dts" 6 | 7 | export default defineConfig(({ mode }) => ({ 8 | build: { 9 | lib: { 10 | entry: resolve(__dirname, `src/index.ts`), 11 | name: `prose`, 12 | fileName: `index`, 13 | }, 14 | minify: mode !== `development`, 15 | sourcemap: true, 16 | emptyOutDir: mode !== `development`, 17 | }, 18 | plugins: [ 19 | { 20 | enforce: `pre`, 21 | ...externals({ 22 | peerDeps: true, 23 | deps: true, 24 | devDeps: true, 25 | }), 26 | }, 27 | dts({ 28 | // rollupTypes: true 29 | }), 30 | ], 31 | })) 32 | -------------------------------------------------------------------------------- /packages/core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig(() => ({ 4 | test: { 5 | environment: "happy-dom", 6 | coverage: { 7 | reportsDirectory: `./.test/coverage`, 8 | }, 9 | }, 10 | })) 11 | -------------------------------------------------------------------------------- /packages/enhancer-annotations/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .test -------------------------------------------------------------------------------- /packages/enhancer-annotations/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prose-reader/enhancer-annotations", 3 | "version": "1.219.0", 4 | "type": "module", 5 | "main": "./dist/index.umd.cjs", 6 | "module": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "files": ["/dist"], 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.js", 12 | "require": "./dist/index.umd.cjs" 13 | } 14 | }, 15 | "scripts": { 16 | "start": "vite build --watch --mode development", 17 | "build": "tsc && vite build" 18 | }, 19 | "peerDependencies": { 20 | "@prose-reader/core": "^1.117.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/enhancer-annotations/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./annotationsEnhancer" 2 | export * from "./types" 3 | export * from "./highlights/Highlight" 4 | -------------------------------------------------------------------------------- /packages/enhancer-annotations/src/report.ts: -------------------------------------------------------------------------------- 1 | import { Report } from "@prose-reader/shared" 2 | import { name } from "../package.json" 3 | 4 | const IS_DEBUG_ENABLED = true 5 | 6 | export const report = Report.namespace(name, IS_DEBUG_ENABLED) 7 | -------------------------------------------------------------------------------- /packages/enhancer-annotations/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "skipLibCheck": true, 17 | "noUncheckedIndexedAccess": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/enhancer-annotations/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path" 2 | import externals from "rollup-plugin-node-externals" 3 | import { defineConfig } from "vite" 4 | import dts from "vite-plugin-dts" 5 | import { name } from "./package.json" 6 | 7 | const libName = name.replace(`@`, ``).replace(`/`, `-`) 8 | 9 | export default defineConfig(({ mode }) => ({ 10 | build: { 11 | minify: false, 12 | target: "esnext", 13 | lib: { 14 | entry: resolve(__dirname, `src/index.ts`), 15 | name: libName, 16 | fileName: `index`, 17 | }, 18 | emptyOutDir: mode !== `development`, 19 | sourcemap: true, 20 | }, 21 | plugins: [ 22 | { 23 | enforce: `pre`, 24 | ...externals({ 25 | peerDeps: true, 26 | deps: true, 27 | devDeps: true, 28 | }), 29 | }, 30 | dts({ 31 | entryRoot: "src", 32 | }), 33 | ], 34 | })) 35 | -------------------------------------------------------------------------------- /packages/enhancer-bookmarks/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .test -------------------------------------------------------------------------------- /packages/enhancer-bookmarks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prose-reader/enhancer-bookmarks", 3 | "version": "1.219.0", 4 | "type": "module", 5 | "main": "./dist/index.umd.cjs", 6 | "module": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/index.js", 11 | "require": "./dist/index.umd.cjs" 12 | } 13 | }, 14 | "files": ["/dist"], 15 | "scripts": { 16 | "start": "vite build --watch --mode development", 17 | "build": "tsc && vite build", 18 | "test": "vitest run --coverage" 19 | }, 20 | "dependencies": { 21 | "@prose-reader/core": "^1.219.0" 22 | }, 23 | "peerDependencies": { 24 | "rxjs": "*" 25 | }, 26 | "devDependencies": { 27 | "rxjs": "*" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/enhancer-bookmarks/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | 3 | test("it works", () => { 4 | expect(true).toBe(true) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/enhancer-bookmarks/src/report.ts: -------------------------------------------------------------------------------- 1 | import { Report } from "@prose-reader/shared" 2 | import { name } from "../package.json" 3 | 4 | const IS_DEBUG_ENABLED = true 5 | 6 | export const report = Report.namespace(name, IS_DEBUG_ENABLED) 7 | -------------------------------------------------------------------------------- /packages/enhancer-bookmarks/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Reader } from "@prose-reader/core" 2 | import type { Observable, ObservedValueOf } from "rxjs" 3 | import type { Commands } from "./Commands" 4 | 5 | export type SerializableBookmark = { 6 | cfi: string 7 | id: string 8 | } 9 | 10 | export type RuntimeBookmark = SerializableBookmark 11 | 12 | export type BookmarksEnhancerAPI = { 13 | readonly __PROSE_READER_ENHANCER_BOOKMARKS: boolean 14 | bookmarks: { 15 | bookmark: Commands["bookmark"] 16 | delete: Commands["delete"] 17 | add: Commands["add"] 18 | bookmarks$: Observable 19 | /** 20 | * Make it conveniant for users to observes pages with bookmarkable status. 21 | */ 22 | pages$: Observable< 23 | (ObservedValueOf["pages"][number] & { 24 | isBookmarkable: boolean | undefined 25 | })[] 26 | > 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/enhancer-bookmarks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.web.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/enhancer-bookmarks/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path" 2 | import externals from "rollup-plugin-node-externals" 3 | import { defineConfig } from "vite" 4 | import dts from "vite-plugin-dts" 5 | import { name } from "./package.json" 6 | 7 | const libName = name.replace(`@`, ``).replace(`/`, `-`) 8 | 9 | export default defineConfig(({ mode }) => ({ 10 | build: { 11 | minify: mode !== `development`, 12 | lib: { 13 | entry: resolve(__dirname, `src/index.ts`), 14 | name: libName, 15 | fileName: `index`, 16 | }, 17 | emptyOutDir: mode !== `development`, 18 | sourcemap: true, 19 | }, 20 | plugins: [ 21 | { 22 | enforce: `pre`, 23 | ...externals({ 24 | peerDeps: true, 25 | deps: true, 26 | devDeps: true, 27 | }), 28 | }, 29 | dts({ 30 | entryRoot: "src", 31 | }), 32 | ], 33 | })) 34 | -------------------------------------------------------------------------------- /packages/enhancer-bookmarks/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig(() => ({ 4 | test: { 5 | coverage: { 6 | reportsDirectory: `./.test/coverage`, 7 | }, 8 | }, 9 | })) 10 | -------------------------------------------------------------------------------- /packages/enhancer-gallery/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .test -------------------------------------------------------------------------------- /packages/enhancer-gallery/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prose-reader/enhancer-gallery", 3 | "version": "1.219.0", 4 | "type": "module", 5 | "main": "./dist/index.umd.cjs", 6 | "module": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/index.js", 11 | "require": "./dist/index.umd.cjs" 12 | } 13 | }, 14 | "files": ["/dist"], 15 | "scripts": { 16 | "start": "vite build --watch --mode development", 17 | "build": "tsc && vite build", 18 | "test": "vitest run --coverage" 19 | }, 20 | "dependencies": { 21 | "@prose-reader/core": "^1.219.0" 22 | }, 23 | "peerDependencies": { 24 | "rxjs": "*" 25 | }, 26 | "devDependencies": { 27 | "rxjs": "*" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/enhancer-gallery/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | 3 | test("it works", () => { 4 | expect(true).toBe(true) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/enhancer-gallery/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Reader } from "@prose-reader/core" 2 | import { Snapshot } from "./Snapshot" 3 | import type { GalleryEnhancerAPI } from "./types" 4 | 5 | export type { GalleryEnhancerAPI } 6 | 7 | export const galleryEnhancer = 8 | ( 9 | next: (options: InheritOptions) => InheritOutput, 10 | ) => 11 | (options: InheritOptions): InheritOutput & GalleryEnhancerAPI => { 12 | const reader = next(options) 13 | 14 | return { 15 | ...reader, 16 | __PROSE_READER_ENHANCER_GALLERY: true, 17 | gallery: { 18 | snapshot: (spineItem, parent, options) => 19 | new Snapshot(reader, spineItem, parent, options), 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/enhancer-gallery/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { SpineItem } from "@prose-reader/core" 2 | import type { Observable } from "rxjs" 3 | 4 | export type GalleryEnhancerAPI = { 5 | readonly __PROSE_READER_ENHANCER_GALLERY: boolean 6 | gallery: { 7 | snapshot: ( 8 | spineItem: SpineItem, 9 | parent: Element, 10 | options: { 11 | height: number 12 | width: number 13 | }, 14 | ) => Observable 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/enhancer-gallery/src/utils/redrawCanvas.ts: -------------------------------------------------------------------------------- 1 | export const redrawCanvas = ( 2 | originalCanvas: HTMLCanvasElement, 3 | clonedCanvas: HTMLCanvasElement, 4 | ) => { 5 | try { 6 | // Copy canvas dimensions 7 | clonedCanvas.width = originalCanvas.width 8 | clonedCanvas.height = originalCanvas.height 9 | 10 | // Copy canvas content 11 | const context = clonedCanvas.getContext("2d") 12 | if (context) { 13 | context.drawImage(originalCanvas, 0, 0) 14 | } 15 | } catch (e) { 16 | console.warn( 17 | "Could not copy canvas content - possible tainted canvas or cross-origin issue", 18 | e, 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/enhancer-gallery/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.web.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/enhancer-gallery/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path" 2 | import externals from "rollup-plugin-node-externals" 3 | import { defineConfig } from "vite" 4 | import dts from "vite-plugin-dts" 5 | import { name } from "./package.json" 6 | 7 | const libName = name.replace(`@`, ``).replace(`/`, `-`) 8 | 9 | export default defineConfig(({ mode }) => ({ 10 | build: { 11 | minify: mode !== `development`, 12 | lib: { 13 | entry: resolve(__dirname, `src/index.ts`), 14 | name: libName, 15 | fileName: `index`, 16 | }, 17 | emptyOutDir: mode !== `development`, 18 | sourcemap: true, 19 | }, 20 | plugins: [ 21 | { 22 | enforce: `pre`, 23 | ...externals({ 24 | peerDeps: true, 25 | deps: true, 26 | devDeps: true, 27 | }), 28 | }, 29 | dts(), 30 | ], 31 | })) 32 | -------------------------------------------------------------------------------- /packages/enhancer-gallery/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig(() => ({ 4 | test: { 5 | coverage: { 6 | reportsDirectory: `./.test/coverage`, 7 | }, 8 | }, 9 | })) 10 | -------------------------------------------------------------------------------- /packages/enhancer-gestures/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /packages/enhancer-gestures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prose-reader/enhancer-gestures", 3 | "version": "1.219.0", 4 | "type": "module", 5 | "main": "./dist/index.cjs", 6 | "module": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "private": false, 9 | "publishConfig": { 10 | "access": "public", 11 | "registry": "https://registry.npmjs.org/" 12 | }, 13 | "exports": { 14 | ".": { 15 | "import": "./dist/index.js", 16 | "require": "./dist/index.umd.cjs" 17 | } 18 | }, 19 | "files": ["/dist"], 20 | "scripts": { 21 | "start": "vite build --watch --mode development", 22 | "build": "tsc && vite build", 23 | "test": "vitest run --coverage" 24 | }, 25 | "dependencies": { 26 | "@prose-reader/core": "^1.219.0" 27 | }, 28 | "devDependencies": { 29 | "gesturx": "*", 30 | "rxjs": "*" 31 | }, 32 | "peerDependencies": { 33 | "gesturx": "1.x", 34 | "rxjs": "7.x" 35 | }, 36 | "gitHead": "4601e14dcacf50b2295cb343582a7ef2c7e1eedc" 37 | } 38 | -------------------------------------------------------------------------------- /packages/enhancer-gestures/src/gestures/zoomPan.ts: -------------------------------------------------------------------------------- 1 | import type { Reader } from "@prose-reader/core" 2 | import type { PanRecognizer } from "gesturx" 3 | import { filter, switchMap, tap } from "rxjs" 4 | 5 | export const registerZoomPan = ({ 6 | reader, 7 | recognizer, 8 | }: { recognizer: PanRecognizer; reader: Reader }) => { 9 | const panStart$ = recognizer.events$.pipe( 10 | filter((event) => event.type === "panStart"), 11 | ) 12 | const panMove$ = recognizer.events$.pipe( 13 | filter((event) => event.type === "panMove"), 14 | ) 15 | 16 | const zoomingPan$ = panStart$.pipe( 17 | switchMap(() => { 18 | const startPosition = reader.zoom.currentPosition 19 | 20 | return panMove$.pipe( 21 | tap((panMoveEvent) => { 22 | if (reader.zoom.isZooming) { 23 | reader.zoom.moveAt({ 24 | x: startPosition.x + panMoveEvent.deltaX, 25 | y: startPosition.y + panMoveEvent.deltaY, 26 | }) 27 | } 28 | }), 29 | ) 30 | }), 31 | ) 32 | 33 | return zoomingPan$ 34 | } 35 | -------------------------------------------------------------------------------- /packages/enhancer-gestures/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | 3 | test("it works", () => { 4 | expect(true).toBe(true) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/enhancer-gestures/src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GestureEvent } from "./types" 2 | import { isHtmlElement } from "@prose-reader/core" 3 | 4 | export const isNotLink = (event: GestureEvent) => { 5 | const target = event.event.target 6 | 7 | if (isHtmlElement(target) && target.tagName === "a") return false 8 | 9 | return true 10 | } 11 | 12 | export const istMatchingSelectors = ( 13 | selectors: string[], 14 | event: GestureEvent, 15 | ): boolean => { 16 | const target = event.event.target 17 | 18 | if (!isHtmlElement(target)) return false 19 | 20 | const match = selectors.find((selector) => { 21 | // Check if the target matches the selector directly 22 | if (target.matches(selector)) return true 23 | 24 | // Check if the target is within an element matching the selector 25 | if (target.closest(selector)) return true 26 | 27 | return false 28 | }) 29 | 30 | return !!match 31 | } 32 | -------------------------------------------------------------------------------- /packages/enhancer-gestures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Bundler", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "skipLibCheck": true, 17 | "noUncheckedIndexedAccess": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/enhancer-gestures/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import dts from "vite-plugin-dts" 3 | import { resolve } from "node:path" 4 | import { name } from "./package.json" 5 | import externals from "rollup-plugin-node-externals" 6 | 7 | const libName = name.replace(`@`, ``).replace(`/`, `-`) 8 | 9 | export default defineConfig(({ mode }) => ({ 10 | build: { 11 | minify: false, 12 | lib: { 13 | entry: resolve(__dirname, `src/index.ts`), 14 | name: libName, 15 | fileName: "index", 16 | }, 17 | emptyOutDir: mode !== `development`, 18 | sourcemap: true, 19 | }, 20 | plugins: [ 21 | { 22 | enforce: `pre`, 23 | ...externals({ 24 | peerDeps: true, 25 | deps: true, 26 | devDeps: true, 27 | }), 28 | }, 29 | dts({ 30 | entryRoot: "src", 31 | }), 32 | ], 33 | })) 34 | -------------------------------------------------------------------------------- /packages/enhancer-gestures/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig(() => ({ 4 | test: { 5 | coverage: { 6 | reportsDirectory: `./.test/coverage`, 7 | }, 8 | }, 9 | })) 10 | -------------------------------------------------------------------------------- /packages/enhancer-pdf/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .test -------------------------------------------------------------------------------- /packages/enhancer-pdf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prose-reader/enhancer-pdf", 3 | "version": "1.219.0", 4 | "type": "module", 5 | "main": "./dist/index.umd.cjs", 6 | "module": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "files": ["/dist"], 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.js", 12 | "require": "./dist/index.umd.cjs" 13 | } 14 | }, 15 | "scripts": { 16 | "start": "vite build --watch --mode development", 17 | "build": "tsc && vite build" 18 | }, 19 | "devDependencies": { 20 | "pdfjs-dist": "^5.1.91" 21 | }, 22 | "peerDependencies": { 23 | "@prose-reader/core": "^1.117.0", 24 | "pdfjs-dist": "5.x", 25 | "rxjs": "*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/enhancer-pdf/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pdfEnhancer" 2 | export * from "./createArchiveFromPdf" 3 | -------------------------------------------------------------------------------- /packages/enhancer-pdf/src/renderer/frame.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | /* This will prevent scrollbars and wrong offset of annotation layer */ 7 | overflow: hidden; 8 | } 9 | -------------------------------------------------------------------------------- /packages/enhancer-pdf/src/types.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import type { Manifest } from "@prose-reader/core" 4 | import type { Archive } from "@prose-reader/streamer" 5 | import type { Observable } from "rxjs" 6 | 7 | export type EnhancerOptions = { 8 | pdf: { 9 | getArchiveForItem: ( 10 | item: Manifest["items"][number], 11 | ) => Observable 12 | pdfjsViewerInlineCss: string 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/enhancer-pdf/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/enhancer-pdf/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import dts from "vite-plugin-dts" 3 | import { resolve } from "node:path" 4 | import { name } from "./package.json" 5 | import externals from "rollup-plugin-node-externals" 6 | 7 | const libName = name.replace(`@`, ``).replace(`/`, `-`) 8 | 9 | export default defineConfig(({ mode }) => ({ 10 | build: { 11 | minify: false, 12 | target: "esnext", 13 | lib: { 14 | entry: resolve(__dirname, `src/index.ts`), 15 | name: libName, 16 | fileName: `index`, 17 | }, 18 | emptyOutDir: mode !== `development`, 19 | sourcemap: true, 20 | }, 21 | plugins: [ 22 | { 23 | enforce: `pre`, 24 | ...externals({ 25 | peerDeps: true, 26 | deps: true, 27 | devDeps: true, 28 | }), 29 | }, 30 | dts(), 31 | ], 32 | })) 33 | -------------------------------------------------------------------------------- /packages/enhancer-search/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .test -------------------------------------------------------------------------------- /packages/enhancer-search/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prose-reader/enhancer-search", 3 | "version": "1.219.0", 4 | "type": "module", 5 | "main": "./dist/index.umd.cjs", 6 | "module": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/index.js", 11 | "require": "./dist/index.umd.cjs" 12 | } 13 | }, 14 | "scripts": { 15 | "start": "vite build --watch --mode development", 16 | "build": "tsc && vite build", 17 | "test": "vitest run --coverage" 18 | }, 19 | "dependencies": { 20 | "@prose-reader/core": "^1.219.0" 21 | }, 22 | "peerDependencies": { 23 | "rxjs": "*" 24 | }, 25 | "gitHead": "4601e14dcacf50b2295cb343582a7ef2c7e1eedc" 26 | } 27 | -------------------------------------------------------------------------------- /packages/enhancer-search/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | 3 | test("it works", () => { 4 | expect(true).toBe(true) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/enhancer-search/src/report.ts: -------------------------------------------------------------------------------- 1 | import { Report } from "@prose-reader/shared" 2 | import { name } from "../package.json" 3 | 4 | const IS_DEBUG_ENABLED = true 5 | 6 | export const report = Report.namespace(name, IS_DEBUG_ENABLED) 7 | -------------------------------------------------------------------------------- /packages/enhancer-search/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Observable } from "rxjs" 2 | import type { SearchResult } from "./search" 3 | 4 | export type ResultItem = { 5 | cfi: string 6 | startCfi?: string 7 | endCfi?: string 8 | } 9 | 10 | export type SearchEnhancerAPI = { 11 | readonly __PROSE_READER_ENHANCER_SEARCH: boolean 12 | search: { 13 | search: (text: string) => Observable 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/enhancer-search/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/enhancer-search/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path" 2 | import externals from "rollup-plugin-node-externals" 3 | import { defineConfig } from "vite" 4 | import dts from "vite-plugin-dts" 5 | import { name } from "./package.json" 6 | 7 | const libName = name.replace(`@`, ``).replace(`/`, `-`) 8 | 9 | export default defineConfig(({ mode }) => ({ 10 | build: { 11 | minify: false, 12 | target: "esnext", 13 | lib: { 14 | entry: resolve(__dirname, `src/index.ts`), 15 | name: libName, 16 | fileName: `index`, 17 | }, 18 | emptyOutDir: mode !== `development`, 19 | sourcemap: true, 20 | }, 21 | plugins: [ 22 | { 23 | enforce: `pre`, 24 | ...externals({ 25 | peerDeps: true, 26 | deps: true, 27 | devDeps: true, 28 | }), 29 | }, 30 | dts({ 31 | entryRoot: "src", 32 | }), 33 | ], 34 | })) 35 | -------------------------------------------------------------------------------- /packages/enhancer-search/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig(() => ({ 4 | test: { 5 | coverage: { 6 | reportsDirectory: `./.test/coverage`, 7 | }, 8 | }, 9 | })) 10 | -------------------------------------------------------------------------------- /packages/react-native/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .test/coverage -------------------------------------------------------------------------------- /packages/react-native/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | 3 | test(`test`, () => { 4 | expect(true).toBe(true) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/react-native/src/native/ReactNativeStreamer.ts: -------------------------------------------------------------------------------- 1 | import { Streamer } from "@prose-reader/streamer" 2 | 3 | export class ReactNativeStreamer extends Streamer { 4 | async fetchResourceAsData({ 5 | key, 6 | resourcePath, 7 | }: { key: string; resourcePath: string }) { 8 | const resource = await super.fetchResource({ key, resourcePath }) 9 | 10 | return { 11 | data: await resource.text(), 12 | headers: Object.fromEntries(resource.headers.entries()), 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-native/src/native/ReaderProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react" 2 | import type { useCreateReader } from "./useCreateReader" 3 | 4 | export const ReaderContext = createContext< 5 | ReturnType | undefined 6 | >(undefined) 7 | 8 | export const useProseReaderContext = () => { 9 | const context = useContext(ReaderContext) 10 | 11 | if (!context) { 12 | throw new Error("useProseReader must be used within a ProseReaderProvider") 13 | } 14 | 15 | return context 16 | } 17 | 18 | export const ReaderProvider = ({ 19 | children, 20 | reader, 21 | }: { 22 | children: React.ReactNode 23 | reader: ReturnType 24 | }) => { 25 | return ( 26 | {children} 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/react-native/src/native/index.ts: -------------------------------------------------------------------------------- 1 | export { useCreateReader } from "./useCreateReader" 2 | export { createArchiveFromExpoFileSystemNext } from "./createArchiveFromExpoFileSystemNext" 3 | export * from "../shared" 4 | export { ReaderProvider } from "./ReaderProvider" 5 | export { useReader } from "./useReader" 6 | export { useReaderState } from "./useReaderState" 7 | export * from "./ReactNativeStreamer" 8 | -------------------------------------------------------------------------------- /packages/react-native/src/native/useReader.ts: -------------------------------------------------------------------------------- 1 | import { useProseReaderContext } from "./ReaderProvider" 2 | 3 | export const useReader = () => { 4 | const { webviewBridge } = useProseReaderContext() 5 | const { postMessage } = webviewBridge 6 | 7 | return { 8 | turnRight: () => { 9 | postMessage("turnRight", undefined) 10 | }, 11 | turnLeft: () => { 12 | postMessage("turnLeft", undefined) 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-native/src/native/useReaderState.ts: -------------------------------------------------------------------------------- 1 | import { useBridge } from "@webview-bridge/react-native" 2 | import type { BridgeState } from "../shared" 3 | import { useProseReaderContext } from "./ReaderProvider" 4 | 5 | export const useReaderState = ( 6 | selector: (state: BridgeState) => T, 7 | ) => { 8 | const { appBridge } = useProseReaderContext() 9 | 10 | return useBridge(appBridge, selector) 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-native/src/shared/useLiveRef.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react" 2 | 3 | export const useLiveRef = (value: T) => { 4 | const ref = useRef(value) 5 | 6 | useEffect(() => { 7 | ref.current = value 8 | }, [value]) 9 | 10 | return ref 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-native/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.rn.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/react-native/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.web.json" }, 5 | { "path": "./tsconfig.rn.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-native/tsconfig.rn.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | }, 6 | "include": ["src/native", "src/shared"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-native/tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.web.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx" 5 | }, 6 | "include": ["src/web", "src/shared"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-native/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path" 2 | import externals from "rollup-plugin-node-externals" 3 | import { defineConfig } from "vite" 4 | import dts from "vite-plugin-dts" 5 | 6 | export default defineConfig(() => { 7 | return { 8 | esbuild: { 9 | // make sure React global is available at RN side. 10 | jsx: "automatic", 11 | }, 12 | build: { 13 | lib: { 14 | entry: { 15 | web: resolve(__dirname, "src/web/index.ts"), 16 | native: resolve(__dirname, "src/native/index.ts"), 17 | shared: resolve(__dirname, "src/shared/index.ts"), 18 | }, 19 | fileName: (format, entryName) => `${entryName}/index.${format}.js`, 20 | }, 21 | }, 22 | plugins: [ 23 | { 24 | enforce: `pre`, 25 | ...externals({ 26 | peerDeps: true, 27 | deps: true, 28 | devDeps: true, 29 | }), 30 | }, 31 | dts({ 32 | entryRoot: "src", 33 | tsconfigPath: "./tsconfig.build.json", 34 | }), 35 | ], 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /packages/react-native/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig(() => ({ 4 | test: { 5 | coverage: { 6 | reportsDirectory: `./.test/coverage`, 7 | }, 8 | }, 9 | })) 10 | -------------------------------------------------------------------------------- /packages/react-reader/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | tsconfig.*.tsbuildinfo -------------------------------------------------------------------------------- /packages/react-reader/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox as ChakraCheckbox } from "@chakra-ui/react" 2 | import * as React from "react" 3 | 4 | export interface CheckboxProps extends ChakraCheckbox.RootProps { 5 | icon?: React.ReactNode 6 | inputProps?: React.InputHTMLAttributes 7 | rootRef?: React.Ref 8 | } 9 | 10 | export const Checkbox = React.forwardRef( 11 | function Checkbox(props, ref) { 12 | const { icon, children, inputProps, rootRef, ...rest } = props 13 | return ( 14 | 15 | 16 | 17 | {icon || } 18 | 19 | {children != null && ( 20 | {children} 21 | )} 22 | 23 | ) 24 | }, 25 | ) 26 | -------------------------------------------------------------------------------- /packages/react-reader/src/components/ui/close-button.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonProps } from "@chakra-ui/react" 2 | import { IconButton as ChakraIconButton } from "@chakra-ui/react" 3 | import * as React from "react" 4 | import { LuX } from "react-icons/lu" 5 | 6 | export type CloseButtonProps = ButtonProps 7 | 8 | export const CloseButton = React.forwardRef< 9 | HTMLButtonElement, 10 | CloseButtonProps 11 | >(function CloseButton(props, ref) { 12 | return ( 13 | 14 | {props.children ?? } 15 | 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/react-reader/src/components/ui/radio.tsx: -------------------------------------------------------------------------------- 1 | import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react" 2 | import * as React from "react" 3 | 4 | export interface RadioProps extends ChakraRadioGroup.ItemProps { 5 | rootRef?: React.Ref 6 | inputProps?: React.InputHTMLAttributes 7 | } 8 | 9 | export const Radio = React.forwardRef( 10 | function Radio(props, ref) { 11 | const { children, inputProps, rootRef, ...rest } = props 12 | return ( 13 | 14 | 15 | 16 | {children && ( 17 | {children} 18 | )} 19 | 20 | ) 21 | }, 22 | ) 23 | 24 | export const RadioGroup = ChakraRadioGroup.Root 25 | -------------------------------------------------------------------------------- /packages/react-reader/src/context/context.ts: -------------------------------------------------------------------------------- 1 | import type { Reader } from "@prose-reader/core" 2 | import { type Context, createContext } from "react" 3 | import { type Signal, signal } from "reactjrx" 4 | import { Subject } from "rxjs" 5 | import type { ReaderNotification } from "../notifications/types" 6 | 7 | type ContextType = { 8 | reader: Reader | undefined 9 | quickMenuSignal: Signal 10 | notificationsSubject: Subject 11 | } 12 | 13 | export const ReaderContext: Context = createContext({ 14 | reader: undefined, 15 | quickMenuSignal: signal({ default: false }), 16 | notificationsSubject: new Subject(), 17 | }) 18 | -------------------------------------------------------------------------------- /packages/react-reader/src/context/useReaderContext.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import { ReaderContext } from "./context" 3 | 4 | export const useReaderContext = () => { 5 | return useContext(ReaderContext) 6 | } 7 | -------------------------------------------------------------------------------- /packages/react-reader/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./context/ReactReaderProvider" 2 | export * from "./ReactReader" 3 | -------------------------------------------------------------------------------- /packages/react-reader/src/navigation/FloatingProgress.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from "@chakra-ui/react" 2 | import { usePagination } from "../pagination/usePagination" 3 | 4 | export const FloatingProgress = () => { 5 | const pagination = usePagination() 6 | const roundedProgress = Math.floor( 7 | (pagination?.percentageEstimateOfBook ?? 0) * 100, 8 | ) 9 | const displayableProgress = roundedProgress > 0 ? roundedProgress : 1 10 | 11 | if (pagination?.percentageEstimateOfBook === undefined) return null 12 | 13 | return ( 14 | 15 | {displayableProgress} % 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/react-reader/src/navigation/FloatingTime.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@chakra-ui/react" 2 | import { TimeIndicator } from "../quickmenu/TimeIndicator" 3 | import { useQuickMenu } from "../quickmenu/useQuickMenu" 4 | 5 | export const FloatingTime = () => { 6 | const [quickMenuOpen] = useQuickMenu() 7 | 8 | return ( 9 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/react-reader/src/navigation/useInterceptExternalLinks.ts: -------------------------------------------------------------------------------- 1 | import { isHtmlTagElement } from "@prose-reader/core" 2 | import { useSubscribe } from "reactjrx" 3 | import { useReader } from "../context/useReader" 4 | 5 | export const useInterceptExternalLinks = () => { 6 | const reader = useReader() 7 | 8 | useSubscribe( 9 | () => 10 | reader?.links$.subscribe((event) => { 11 | if (event.type === "click" && isHtmlTagElement(event.target, "a")) { 12 | if (!event.target.href) return 13 | 14 | const url = new URL(event.target.href) 15 | 16 | if (window.location.host !== url.host) { 17 | const response = confirm( 18 | `You are going to be redirected to external link`, 19 | ) 20 | 21 | if (response) { 22 | window.open(event.target.href, "__blank") 23 | } 24 | } 25 | } 26 | }), 27 | [reader], 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /packages/react-reader/src/notifications/types.ts: -------------------------------------------------------------------------------- 1 | import type { Observable } from "rxjs" 2 | 3 | export type ReaderNotification = { 4 | key: string 5 | title: string 6 | description?: string 7 | duration?: number 8 | abort?: Observable 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-reader/src/pagination/usePagination.ts: -------------------------------------------------------------------------------- 1 | import type { Reader } from "@prose-reader/core" 2 | import { useObserve } from "reactjrx" 3 | import { NEVER, combineLatest, map } from "rxjs" 4 | import { useReader } from "../context/useReader" 5 | 6 | export const usePagination = (): 7 | | (Reader["pagination"]["state"] & { hasChapters: boolean }) 8 | | undefined => { 9 | const reader = useReader() 10 | 11 | return useObserve( 12 | () => 13 | !reader 14 | ? NEVER 15 | : combineLatest([reader.pagination.state$, reader.context.state$]).pipe( 16 | map(([state, context]) => { 17 | const isOnlyImages = context.manifest?.spineItems.every((item) => 18 | item.mediaType?.startsWith("image/"), 19 | ) 20 | 21 | return { 22 | ...state, 23 | hasChapters: !context.isFullyPrePaginated && !isOnlyImages, 24 | } 25 | }), 26 | ), 27 | [reader], 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /packages/react-reader/src/quickmenu/QuickMenu.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react" 2 | import { BottomBar } from "./BottomBar" 3 | import { TopBar } from "./TopBar" 4 | import { useQuickMenu } from "./useQuickMenu" 5 | 6 | export const QuickMenu = memo( 7 | ({ 8 | onItemClick, 9 | }: { 10 | onItemClick: ( 11 | item: 12 | | "annotations" 13 | | "search" 14 | | "help" 15 | | "toc" 16 | | "bookmarks" 17 | | "more" 18 | | "back" 19 | | "gallery", 20 | ) => void 21 | }) => { 22 | const [quickMenuOpen] = useQuickMenu() 23 | 24 | return ( 25 | <> 26 | 27 | 28 | 29 | ) 30 | }, 31 | ) 32 | -------------------------------------------------------------------------------- /packages/react-reader/src/quickmenu/TimeIndicator.tsx: -------------------------------------------------------------------------------- 1 | import { Text, type TextProps } from "@chakra-ui/react" 2 | import { useEffect, useState } from "react" 3 | 4 | export const useTime = () => { 5 | const [time, setTime] = useState(new Date()) 6 | 7 | useEffect(() => { 8 | const interval = setInterval(() => { 9 | setTime(new Date()) 10 | }, 1000 * 60) 11 | 12 | return () => clearInterval(interval) 13 | }, []) 14 | 15 | return time 16 | } 17 | 18 | export const TimeIndicator = (props: TextProps) => { 19 | const time = useTime() 20 | 21 | return ( 22 | 23 | {time.toLocaleTimeString(navigator.language, { 24 | hour: "2-digit", 25 | minute: "2-digit", 26 | })} 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /packages/react-reader/src/quickmenu/useQuickMenu.ts: -------------------------------------------------------------------------------- 1 | import { useSignalValue } from "reactjrx" 2 | import { useReaderContext } from "../context/useReaderContext" 3 | 4 | export const useQuickMenu = () => { 5 | const { quickMenuSignal } = useReaderContext() 6 | 7 | const quickMenu = useSignalValue(quickMenuSignal) 8 | 9 | return [quickMenu, quickMenuSignal.setValue, quickMenuSignal] as const 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-reader/src/settings/useSettings.ts: -------------------------------------------------------------------------------- 1 | import type { Reader } from "@prose-reader/core" 2 | import { useObserve } from "reactjrx" 3 | import { useReader } from "../context/useReader" 4 | 5 | export const useSettings = (): Reader["settings"]["values"] | undefined => { 6 | const reader = useReader() 7 | 8 | return useObserve(() => reader?.settings.values$, [reader]) 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-reader/src/theme/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@chakra-ui/react" 2 | 3 | export const config = defineConfig({ 4 | theme: { 5 | semanticTokens: { 6 | colors: { 7 | danger: { 8 | value: { base: "{colors.red}", _dark: "{colors.darkred}" }, 9 | }, 10 | }, 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /packages/react-reader/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/react-reader/src/zoom/useNotifyZoom.ts: -------------------------------------------------------------------------------- 1 | import { useSubscribe } from "reactjrx" 2 | import { EMPTY, NEVER, Subject, finalize, switchMap } from "rxjs" 3 | import { useReader } from "../context/useReader" 4 | import { useReaderContext } from "../context/useReaderContext" 5 | 6 | export const useNotifyZoom = () => { 7 | const reader = useReader() 8 | const { notificationsSubject } = useReaderContext() 9 | 10 | useSubscribe( 11 | () => 12 | reader?.zoom.isZooming$.pipe( 13 | switchMap((isZooming) => { 14 | if (!isZooming) return EMPTY 15 | 16 | const abort = new Subject() 17 | 18 | notificationsSubject.next({ 19 | key: "zoom", 20 | title: "Zooming", 21 | duration: 999999, 22 | abort: abort, 23 | }) 24 | 25 | return NEVER.pipe( 26 | finalize(() => { 27 | abort.next() 28 | abort.complete() 29 | }), 30 | ) 31 | }), 32 | ), 33 | [reader], 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /packages/react-reader/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.web.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "jsx": "react-jsx", 6 | "outDir": "dist" 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-reader/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-reader/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.node.json", 3 | "compilerOptions": { 4 | "skipLibCheck": true 5 | }, 6 | "include": ["vite.config.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-reader/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path" 2 | import react from "@vitejs/plugin-react" 3 | import externals from "rollup-plugin-node-externals" 4 | import { defineConfig } from "vite" 5 | import dts from "vite-plugin-dts" 6 | 7 | // https://vite.dev/config/ 8 | export default defineConfig(({ mode }) => ({ 9 | build: { 10 | lib: { 11 | entry: resolve(__dirname, "src/index.ts"), 12 | name: "prose-react-reader", 13 | fileName: `index`, 14 | }, 15 | emptyOutDir: mode !== "development", 16 | sourcemap: true, 17 | }, 18 | plugins: [ 19 | { 20 | enforce: `pre`, 21 | ...externals({ 22 | peerDeps: true, 23 | deps: true, 24 | devDeps: true, 25 | }), 26 | }, 27 | react(), 28 | dts({ 29 | tsconfigPath: "./tsconfig.app.json", 30 | entryRoot: "src", 31 | }), 32 | ], 33 | })) 34 | -------------------------------------------------------------------------------- /packages/shared/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .test/coverage -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prose-reader/shared", 3 | "version": "1.219.0", 4 | "type": "module", 5 | "main": "./dist/index.umd.cjs", 6 | "module": "./dist/index.js", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/index.js", 10 | "require": "./dist/index.umd.cjs" 11 | } 12 | }, 13 | "types": "./dist/index.d.ts", 14 | "license": "MIT", 15 | "files": ["/dist"], 16 | "scripts": { 17 | "start": "vite build --watch --mode development", 18 | "build": "tsc && vite build", 19 | "test": "vitest run --coverage" 20 | }, 21 | "gitHead": "4601e14dcacf50b2295cb343582a7ef2c7e1eedc" 22 | } 23 | -------------------------------------------------------------------------------- /packages/shared/src/array.ts: -------------------------------------------------------------------------------- 1 | // biome-ignore lint/suspicious/noExplicitAny: 2 | export const arrayEqual = (a: A, b: B) => { 3 | // @ts-expect-error 4 | if (a === b) return true 5 | if (a.length !== b.length) return false 6 | 7 | return a.every((v, i) => v === b[i]) 8 | } 9 | -------------------------------------------------------------------------------- /packages/shared/src/contentType.ts: -------------------------------------------------------------------------------- 1 | import { getUrlExtension } from "./url" 2 | 3 | export const detectMimeTypeFromName = (name: string) => { 4 | const extension = getUrlExtension(name) 5 | 6 | switch (extension) { 7 | case `png`: 8 | return `image/png` 9 | case `jpg`: 10 | return `image/jpg` 11 | case `jpeg`: 12 | return `image/jpeg` 13 | case `txt`: 14 | return `text/plain` 15 | case `webp`: 16 | return `image/webp` 17 | case `xhtml`: 18 | return `application/xhtml+xml` 19 | } 20 | 21 | return undefined 22 | } 23 | 24 | export const isXmlBasedMimeType = ({ 25 | mimeType, 26 | uri, 27 | }: { uri?: string; mimeType?: string }) => { 28 | const _mimeType = mimeType ?? detectMimeTypeFromName(uri ?? "") 29 | 30 | return _mimeType?.startsWith(`application/xhtml+xml`) 31 | } 32 | 33 | export const parseContentType = (str: string) => { 34 | if (!str.length) return undefined 35 | 36 | const cut = str.indexOf(`;`) 37 | 38 | return cut ? str.substring(0, str.indexOf(`;`)) : str 39 | } 40 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { Manifest, TocItem } from "./Manifest" 2 | export * from "./contentType" 3 | export * from "./url" 4 | export * from "./resources" 5 | export * from "./objects" 6 | export * from "./report" 7 | export * from "./array" 8 | -------------------------------------------------------------------------------- /packages/shared/src/resources.ts: -------------------------------------------------------------------------------- 1 | export const PROSE_READER_RESOURCE_ERROR_INJECTED_META_NAME = 2 | "prose-reader-resource-error" 3 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.web.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "noEmit": true, 6 | "declaration": true, 7 | "isolatedModules": true, 8 | "moduleResolution": "bundler" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/shared/vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import { resolve } from "node:path" 3 | import { defineConfig } from "vite" 4 | import dts from "vite-plugin-dts" 5 | 6 | export default defineConfig(({ mode }) => { 7 | return { 8 | build: { 9 | lib: { 10 | // Could also be a dictionary or array of multiple entry points 11 | entry: resolve(__dirname, `src/index.ts`), 12 | name: `prose-shared`, 13 | fileName: `index`, 14 | }, 15 | emptyOutDir: mode !== "development", 16 | sourcemap: true, 17 | rollupOptions: { 18 | // make sure to externalize deps that shouldn't be bundled 19 | // into your library 20 | external: [`rxjs`], 21 | output: { 22 | // Provide global variables to use in the UMD build 23 | // for externalized deps 24 | globals: { 25 | // vue: `Vue`, 26 | }, 27 | }, 28 | }, 29 | }, 30 | plugins: [ 31 | dts({ 32 | entryRoot: "src", 33 | }), 34 | ], 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /packages/shared/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig(() => ({ 4 | test: { 5 | coverage: { 6 | reportsDirectory: `./.test/coverage`, 7 | }, 8 | }, 9 | })) 10 | -------------------------------------------------------------------------------- /packages/streamer/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .test -------------------------------------------------------------------------------- /packages/streamer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prose-reader/streamer", 3 | "version": "1.219.0", 4 | "type": "module", 5 | "main": "./dist/index.umd.cjs", 6 | "module": "./dist/index.js", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/index.js", 10 | "require": "./dist/index.umd.cjs" 11 | } 12 | }, 13 | "types": "./dist/index.d.ts", 14 | "license": "MIT", 15 | "files": ["/dist"], 16 | "scripts": { 17 | "start": "vite build --watch --mode development", 18 | "build": "tsc && vite build", 19 | "test": "vitest run --coverage", 20 | "tsc": "tsc", 21 | "test:watch": "vitest watch" 22 | }, 23 | "dependencies": { 24 | "@prose-reader/shared": "^1.219.0", 25 | "xmldoc": "^2.0.0" 26 | }, 27 | "peerDependencies": { 28 | "buffer": "^6.0.3", 29 | "rxjs": "*" 30 | }, 31 | "gitHead": "4601e14dcacf50b2295cb343582a7ef2c7e1eedc", 32 | "devDependencies": { 33 | "buffer": "^6.0.3", 34 | "isomorphic-fetch": "^3.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/streamer/src/Streamer.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { Streamer } from "." 3 | 4 | describe("Given custom error on get Archive", () => { 5 | it("should return correct error", async () => { 6 | class MyError extends Error {} 7 | 8 | const streamer = new Streamer({ 9 | cleanArchiveAfter: 1, 10 | getArchive: async () => { 11 | throw new MyError() 12 | }, 13 | onError: (error) => { 14 | if (error instanceof MyError) 15 | return new Response(`myError`, { status: 500 }) 16 | 17 | return new Response(``, { status: 500 }) 18 | }, 19 | }) 20 | 21 | const response = await streamer.fetchManifest({ key: "any" }) 22 | 23 | expect(await response.text()).toBe(`myError`) 24 | expect(response.status).toBe(500) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/streamer/src/archives/types.ts: -------------------------------------------------------------------------------- 1 | export interface StreamResult { 2 | on(e: `data`, cb: (data: Uint8Array) => void): void 3 | on(e: `error`, cb: (error: Error) => void): void 4 | on(e: `end`, cb: () => void): void 5 | resume(): void 6 | } 7 | 8 | type FileRecord = { 9 | dir: false 10 | basename: string 11 | uri: string 12 | blob: () => Promise 13 | string: () => Promise 14 | stream?: () => StreamResult 15 | size: number 16 | // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types 17 | encodingFormat?: string 18 | } 19 | 20 | type DirectoryRecord = { 21 | dir: true 22 | basename: string 23 | uri: string 24 | size: number 25 | encodingFormat?: undefined 26 | } 27 | 28 | export type Archive = { 29 | filename: string 30 | records: (FileRecord | DirectoryRecord)[] 31 | close: () => Promise 32 | } 33 | -------------------------------------------------------------------------------- /packages/streamer/src/configure.ts: -------------------------------------------------------------------------------- 1 | import { Report } from "./report" 2 | 3 | export const configure = ({ 4 | enableReport, 5 | }: { enableReport?: boolean } = {}) => { 6 | Report.enable(!!enableReport) 7 | } 8 | -------------------------------------------------------------------------------- /packages/streamer/src/epubs/getArchiveOpfInfo.ts: -------------------------------------------------------------------------------- 1 | import type { Archive } from "../archives/types" 2 | 3 | export const getArchiveOpfInfo = (archive: Archive) => { 4 | const filesAsArray = Object.values(archive.records).filter( 5 | (file) => !file.dir, 6 | ) 7 | const file = filesAsArray.find((file) => file.uri.endsWith(`.opf`)) 8 | 9 | return { 10 | data: file, 11 | basePath: file?.uri.substring(0, file.uri.lastIndexOf(`/`)) || ``, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/streamer/src/epubs/isArchiveEpub.ts: -------------------------------------------------------------------------------- 1 | import type { Archive } from "../archives/types" 2 | 3 | export const isArchiveEpub = (archive: Archive) => { 4 | return archive.records.some((file) => file.basename.endsWith(`.opf`)) 5 | } 6 | -------------------------------------------------------------------------------- /packages/streamer/src/generators/manifest/hooks/comicInfo.ts: -------------------------------------------------------------------------------- 1 | import type { Manifest } from "@prose-reader/shared" 2 | import type { Archive } from "../../../archives/types" 3 | import { extractKoboInformationFromArchive } from "../../../parsers/kobo" 4 | 5 | /** 6 | * Handle archive which contains ComicInfo.xml. This is a meta file 7 | * used to define cbz, etc. I believe it comes from some sites or apps. 8 | */ 9 | export const comicInfoHook = 10 | ({ archive }: { archive: Archive; baseUrl: string }) => 11 | async (manifest: Manifest): Promise => { 12 | const koboInformation = await extractKoboInformationFromArchive(archive) 13 | 14 | return { 15 | ...manifest, 16 | renditionLayout: 17 | manifest.renditionLayout ?? koboInformation.renditionLayout, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/streamer/src/generators/resources/hooks/cssFixHook.ts: -------------------------------------------------------------------------------- 1 | import type { Archive } from "../../../archives/types" 2 | import type { HookResource } from "./types" 3 | 4 | export const cssFixHook = 5 | ({ archive, resourcePath }: { archive: Archive; resourcePath: string }) => 6 | async (resource: HookResource): Promise => { 7 | const file = Object.values(archive.records).find( 8 | (file) => file.uri === resourcePath && !file.dir, 9 | ) 10 | 11 | if (file && !file.dir && file.basename.endsWith(`.css`)) { 12 | const bodyToParse = resource.body ?? (await file.string()) 13 | 14 | /** 15 | * Fix the potentially invalid writing mode present on some vertical book. 16 | * This has the benefit of making it compatible with firefox as well. 17 | */ 18 | const newBody = bodyToParse.replaceAll( 19 | `-webkit-writing-mode`, 20 | `writing-mode`, 21 | ) 22 | 23 | return { 24 | ...resource, 25 | body: newBody, 26 | } 27 | } 28 | 29 | return resource 30 | } 31 | -------------------------------------------------------------------------------- /packages/streamer/src/generators/resources/hooks/types.ts: -------------------------------------------------------------------------------- 1 | export type HookResource = { 2 | body?: string 3 | params: { 4 | contentType?: string 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/streamer/src/index.ts: -------------------------------------------------------------------------------- 1 | export { generateResourceFromArchive } from "./generators/resources" 2 | export { generateManifestFromArchive } from "./generators/manifest" 3 | 4 | export { getArchiveOpfInfo } from "./epubs/getArchiveOpfInfo" 5 | 6 | export { createArchiveFromUrls } from "./archives/createArchiveFromUrls" 7 | export { createArchiveFromText } from "./archives/createArchiveFromText" 8 | export { createArchiveFromJszip } from "./archives/createArchiveFromJszip" 9 | export { createArchiveFromLibArchive } from "./archives/createArchiveFromLibArchive" 10 | export { createArchiveFromArrayBufferList } from "./archives/createArchiveFromArrayBufferList" 11 | 12 | export type { Manifest } from "@prose-reader/shared" 13 | export type { Archive } from "./archives/types" 14 | 15 | export { configure } from "./configure" 16 | export { Streamer } from "./Streamer" 17 | export { ServiceWorkerStreamer } from "./ServiceWorkerStreamer" 18 | 19 | export * from "./utils/sortByTitleComparator" 20 | export * from "./utils/uri" 21 | -------------------------------------------------------------------------------- /packages/streamer/src/parsers/kobo.ts: -------------------------------------------------------------------------------- 1 | import { XmlDocument } from "xmldoc" 2 | import type { Archive } from ".." 3 | 4 | type KoboInformation = { 5 | renditionLayout?: `reflowable` | `pre-paginated` | undefined 6 | } 7 | 8 | export const extractKoboInformationFromArchive = async (archive: Archive) => { 9 | const koboInformation: KoboInformation = { 10 | renditionLayout: undefined, 11 | } 12 | 13 | await Promise.all( 14 | archive.records.map(async (file) => { 15 | if (file.uri.endsWith(`com.kobobooks.display-options.xml`) && !file.dir) { 16 | const opfXmlDoc = new XmlDocument(await file.string()) 17 | const optionElement = opfXmlDoc 18 | .childNamed(`platform`) 19 | ?.childNamed(`option`) 20 | if ( 21 | optionElement?.attr?.name === `fixed-layout` && 22 | optionElement.val === `true` 23 | ) { 24 | koboInformation.renditionLayout = `pre-paginated` 25 | } 26 | } 27 | }), 28 | ) 29 | 30 | return koboInformation 31 | } 32 | -------------------------------------------------------------------------------- /packages/streamer/src/parsers/xml.ts: -------------------------------------------------------------------------------- 1 | import { XmlTextNode } from "xmldoc" 2 | 3 | import { XmlElement } from "xmldoc" 4 | 5 | export const getXmlElementInnerText = ( 6 | node: XmlElement | undefined, 7 | ): string => { 8 | if (!node) return "" 9 | 10 | return node.children 11 | .map((child) => { 12 | if (child instanceof XmlTextNode) return child.text 13 | if (child instanceof XmlElement) return getXmlElementInnerText(child) 14 | return "" 15 | }) 16 | .join("") 17 | .trim() 18 | } 19 | -------------------------------------------------------------------------------- /packages/streamer/src/report.ts: -------------------------------------------------------------------------------- 1 | let enabled = false 2 | 3 | export const Report = { 4 | enable: (enable: boolean) => { 5 | enabled = enable 6 | }, 7 | // biome-ignore lint/suspicious/noExplicitAny: 8 | log: (...data: any[]) => { 9 | if (enabled) { 10 | console.log(`[prose-reader-streamer]`, ...data) 11 | } 12 | }, 13 | // biome-ignore lint/suspicious/noExplicitAny: 14 | debug: (...data: any[]) => { 15 | if (enabled) { 16 | console.debug(`[prose-reader-streamer]`, ...data) 17 | } 18 | }, 19 | // biome-ignore lint/suspicious/noExplicitAny: 20 | warn: (...data: any[]) => { 21 | if (enabled) { 22 | console.warn(`[prose-reader-streamer]`, ...data) 23 | } 24 | }, 25 | // biome-ignore lint/suspicious/noExplicitAny: 26 | error: (...data: any[]) => { 27 | console.error(...data) 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /packages/streamer/src/tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | import "isomorphic-fetch" 2 | -------------------------------------------------------------------------------- /packages/streamer/src/tests/waitFor.ts: -------------------------------------------------------------------------------- 1 | export const waitFor = (timer: number) => 2 | new Promise((resolve) => setTimeout(resolve, timer)) 3 | -------------------------------------------------------------------------------- /packages/streamer/src/utils/sortByTitleComparator.ts: -------------------------------------------------------------------------------- 1 | export const sortByTitleComparator = (a: string, b: string) => { 2 | const alist = a.split(/(\d+)/) 3 | const blist = b.split(/(\d+)/) 4 | 5 | for (let i = 0, len = alist.length; i < len; i++) { 6 | if (alist[i] !== blist[i]) { 7 | if (alist[i]?.match(/\d/)) { 8 | return +(alist[i] || ``) - +(blist[i] || ``) 9 | } 10 | return (alist[i] || ``).localeCompare(blist[i] || ``) 11 | } 12 | } 13 | 14 | return 1 15 | } 16 | -------------------------------------------------------------------------------- /packages/streamer/src/utils/uri.ts: -------------------------------------------------------------------------------- 1 | export const getUriBasename = (uri: string) => 2 | uri.substring(uri.lastIndexOf(`/`) + 1) || uri 3 | 4 | export const removeTrailingSlash = (uri: string) => 5 | uri.endsWith("/") ? uri.slice(0, -1) : uri 6 | 7 | export const getUriBasePath = (uri: string) => { 8 | const lastSlashIndex = uri.lastIndexOf("/") 9 | 10 | return lastSlashIndex >= 0 ? uri.substring(0, lastSlashIndex) : "" 11 | } 12 | -------------------------------------------------------------------------------- /packages/streamer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es6", 7 | "lib": ["DOM", "esnext"], 8 | "strict": true, 9 | "allowJs": false, 10 | "moduleResolution": "Bundler", 11 | "allowSyntheticDefaultImports": true, 12 | "noUncheckedIndexedAccess": true, 13 | "noEmit": true, 14 | "declaration": true, 15 | "isolatedModules": true, 16 | "stripInternal": true, 17 | "skipLibCheck": true 18 | }, 19 | "exclude": ["dist", "node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/streamer/vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import { resolve } from "node:path" 3 | import { defineConfig } from "vite" 4 | import dts from "vite-plugin-dts" 5 | import externals from "rollup-plugin-node-externals" 6 | 7 | export default defineConfig(({ mode }) => ({ 8 | build: { 9 | lib: { 10 | entry: resolve(__dirname, `src/index.ts`), 11 | name: `prose-streamer`, 12 | fileName: `index`, 13 | }, 14 | sourcemap: true, 15 | minify: mode === "development" ? false : "esbuild", 16 | emptyOutDir: mode !== "development", 17 | }, 18 | plugins: [ 19 | { 20 | enforce: `pre`, 21 | ...externals({ 22 | peerDeps: true, 23 | deps: true, 24 | devDeps: true, 25 | }), 26 | }, 27 | dts({ 28 | entryRoot: "src", 29 | }), 30 | ], 31 | })) 32 | -------------------------------------------------------------------------------- /packages/streamer/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig(() => ({ 4 | test: { 5 | coverage: { 6 | reportsDirectory: `./.test/coverage`, 7 | }, 8 | }, 9 | })) 10 | -------------------------------------------------------------------------------- /prose-react-native-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | expo-env.d.ts 11 | 12 | # Native 13 | .kotlin/ 14 | *.orig.* 15 | *.jks 16 | *.p8 17 | *.p12 18 | *.key 19 | *.mobileprovision 20 | 21 | # Metro 22 | .metro-health-check* 23 | 24 | # debug 25 | npm-debug.* 26 | yarn-debug.* 27 | yarn-error.* 28 | 29 | # macOS 30 | .DS_Store 31 | *.pem 32 | 33 | # local env files 34 | .env*.local 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | 39 | app-example 40 | -------------------------------------------------------------------------------- /prose-react-native-demo/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": "explicit", 4 | "source.organizeImports": "explicit", 5 | "source.sortMembers": "explicit" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /prose-react-native-demo/android/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Android/IntelliJ 6 | # 7 | build/ 8 | .idea 9 | .gradle 10 | local.properties 11 | *.iml 12 | *.hprof 13 | .cxx/ 14 | 15 | # Bundle artifacts 16 | *.jsbundle 17 | -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/debug.keystore -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # react-native-reanimated 11 | -keep class com.swmansion.reanimated.** { *; } 12 | -keep class com.facebook.react.turbomodule.** { *; } 13 | 14 | # Add any project specific keep options here: 15 | -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff 3 | #ffffff 4 | #023c69 5 | #ffffff 6 | -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | prose-react-native-demo 3 | automatic 4 | contain 5 | false 6 | -------------------------------------------------------------------------------- /prose-react-native-demo/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | -------------------------------------------------------------------------------- /prose-react-native-demo/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /prose-react-native-demo/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /prose-react-native-demo/app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { useColorScheme } from "@/components/useColorScheme" 2 | import { 3 | DarkTheme, 4 | DefaultTheme, 5 | ThemeProvider, 6 | } from "@react-navigation/native" 7 | import { useFonts } from "expo-font" 8 | import { Stack } from "expo-router" 9 | import { StatusBar } from "expo-status-bar" 10 | import "react-native-reanimated" 11 | 12 | export default function RootLayout() { 13 | const colorScheme = useColorScheme() 14 | const [loaded] = useFonts({ 15 | SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"), 16 | }) 17 | 18 | if (!loaded) { 19 | // Async font loading only occurs in development. 20 | return null 21 | } 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /prose-react-native-demo/assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /prose-react-native-demo/assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /prose-react-native-demo/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/assets/images/favicon.png -------------------------------------------------------------------------------- /prose-react-native-demo/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/assets/images/icon.png -------------------------------------------------------------------------------- /prose-react-native-demo/assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /prose-react-native-demo/assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/assets/images/react-logo.png -------------------------------------------------------------------------------- /prose-react-native-demo/assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /prose-react-native-demo/assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /prose-react-native-demo/assets/images/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/assets/images/splash-icon.png -------------------------------------------------------------------------------- /prose-react-native-demo/components/constants.ts: -------------------------------------------------------------------------------- 1 | import { Directory, Paths } from "expo-file-system/next" 2 | 3 | export const epubsDestination = new Directory(Paths.cache, "epubs") 4 | export const epubsDownloadsDestination = new Directory( 5 | epubsDestination, 6 | "downloads", 7 | ) 8 | export const unzippedDestination = new Directory(epubsDestination, "raw") 9 | -------------------------------------------------------------------------------- /prose-react-native-demo/components/reader/Streamer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ReactNativeStreamer, 3 | createArchiveFromExpoFileSystemNext, 4 | } from "@prose-reader/react-native" 5 | import { Directory } from "expo-file-system/next" 6 | import { unzippedDestination } from "../constants" 7 | 8 | export const streamer = new ReactNativeStreamer({ 9 | getArchive: async (epubFolderName) => { 10 | const archive = await createArchiveFromExpoFileSystemNext( 11 | new Directory(unzippedDestination, epubFolderName), 12 | { 13 | orderByAlpha: true, 14 | name: "archive.zip", 15 | }, 16 | ) 17 | 18 | return archive 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /prose-react-native-demo/components/reader/TopMenu.tsx: -------------------------------------------------------------------------------- 1 | import { useReaderState } from "@prose-reader/react-native" 2 | import { SafeAreaView, StyleSheet, Text } from "react-native" 3 | 4 | export const TopMenu = () => { 5 | const manifest = useReaderState((state) => state.context?.manifest) 6 | 7 | return ( 8 | 9 | {manifest?.title} 10 | 11 | ) 12 | } 13 | 14 | const styles = StyleSheet.create({ 15 | container: { 16 | display: "flex", 17 | position: "absolute", 18 | top: 0, 19 | left: 0, 20 | right: 0, 21 | backgroundColor: "white", 22 | borderWidth: 1, 23 | borderColor: "red", 24 | justifyContent: "center", 25 | alignItems: "center", 26 | }, 27 | title: { 28 | fontSize: 12, 29 | padding: 10, 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /prose-react-native-demo/components/reader/useWebviewHtmlAsset.ts: -------------------------------------------------------------------------------- 1 | import { useAssets } from "expo-asset" 2 | import { readAsStringAsync } from "expo-file-system" 3 | import { useEffect, useState } from "react" 4 | 5 | export function useWebviewHtmlAsset() { 6 | const [html, setHtml] = useState() 7 | const [assets] = useAssets([require("@/assets/index.html")]) 8 | 9 | useEffect(() => { 10 | ;(async () => { 11 | const asset = assets?.[0] 12 | 13 | if (asset?.localUri) { 14 | const fileContents = await readAsStringAsync(asset.localUri) 15 | setHtml(fileContents) 16 | } 17 | })() 18 | }, [assets]) 19 | 20 | return { html } 21 | } 22 | -------------------------------------------------------------------------------- /prose-react-native-demo/components/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | export { useColorScheme } from "react-native" 2 | -------------------------------------------------------------------------------- /prose-react-native-demo/components/useDownloadFile.ts: -------------------------------------------------------------------------------- 1 | import { File } from "expo-file-system/next" 2 | import { useEffect, useState } from "react" 3 | import { epubsDownloadsDestination } from "./constants" 4 | 5 | export const useDownloadFile = (url: string) => { 6 | const [file, setFile] = useState(null) 7 | 8 | useEffect(() => { 9 | let isMounted = true 10 | ;(async () => { 11 | setFile(null) 12 | 13 | const filename = `${url.split("/").pop() ?? ""}` 14 | const downloadedFile = new File(epubsDownloadsDestination, filename) 15 | 16 | if (!downloadedFile.exists) { 17 | const file = await File.downloadFileAsync( 18 | "https://www.gutenberg.org/ebooks/76073.epub3.images", 19 | epubsDownloadsDestination, 20 | ) 21 | 22 | if (!isMounted) return 23 | 24 | file.move(downloadedFile) 25 | } 26 | 27 | setFile(downloadedFile) 28 | })() 29 | 30 | return () => { 31 | isMounted = false 32 | } 33 | }, [url]) 34 | 35 | return file 36 | } 37 | -------------------------------------------------------------------------------- /prose-react-native-demo/eslint.config.js: -------------------------------------------------------------------------------- 1 | // https://docs.expo.dev/guides/using-eslint/ 2 | const { defineConfig } = require("eslint/config") 3 | const expoConfig = require("eslint-config-expo/flat") 4 | 5 | module.exports = defineConfig([ 6 | expoConfig, 7 | { 8 | ignores: ["dist/*"], 9 | }, 10 | ]) 11 | -------------------------------------------------------------------------------- /prose-react-native-demo/ios/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | .xcode.env.local 25 | 26 | # Bundle artifacts 27 | *.jsbundle 28 | 29 | # CocoaPods 30 | /Pods/ 31 | -------------------------------------------------------------------------------- /prose-react-native-demo/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /prose-react-native-demo/ios/Podfile.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo.jsEngine": "hermes", 3 | "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true", 4 | "newArchEnabled": "true", 5 | "ios.deploymentTarget": "15.5", 6 | "apple.privacyManifestAggregationEnabled": "true" 7 | } 8 | -------------------------------------------------------------------------------- /prose-react-native-demo/ios/prosereactnativedemo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /prose-react-native-demo/ios/prosereactnativedemo/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/ios/prosereactnativedemo/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png -------------------------------------------------------------------------------- /prose-react-native-demo/ios/prosereactnativedemo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "App-Icon-1024x1024@1x.png", 5 | "idiom": "universal", 6 | "platform": "ios", 7 | "size": "1024x1024" 8 | } 9 | ], 10 | "info": { 11 | "version": 1, 12 | "author": "expo" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /prose-react-native-demo/ios/prosereactnativedemo/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": 1, 4 | "author": "expo" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /prose-react-native-demo/ios/prosereactnativedemo/Images.xcassets/SplashScreenBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": [ 3 | { 4 | "color": { 5 | "components": { 6 | "alpha": "1.000", 7 | "blue": "1.00000000000000", 8 | "green": "1.00000000000000", 9 | "red": "1.00000000000000" 10 | }, 11 | "color-space": "srgb" 12 | }, 13 | "idiom": "universal" 14 | } 15 | ], 16 | "info": { 17 | "version": 1, 18 | "author": "expo" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /prose-react-native-demo/ios/prosereactnativedemo/Images.xcassets/SplashScreenLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "filename": "image@2x.png", 11 | "scale": "2x" 12 | }, 13 | { 14 | "idiom": "universal", 15 | "filename": "image@3x.png", 16 | "scale": "3x" 17 | } 18 | ], 19 | "info": { 20 | "version": 1, 21 | "author": "expo" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /prose-react-native-demo/ios/prosereactnativedemo/Images.xcassets/SplashScreenLogo.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/ios/prosereactnativedemo/Images.xcassets/SplashScreenLogo.imageset/image.png -------------------------------------------------------------------------------- /prose-react-native-demo/ios/prosereactnativedemo/Images.xcassets/SplashScreenLogo.imageset/image@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/ios/prosereactnativedemo/Images.xcassets/SplashScreenLogo.imageset/image@2x.png -------------------------------------------------------------------------------- /prose-react-native-demo/ios/prosereactnativedemo/Images.xcassets/SplashScreenLogo.imageset/image@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbret/prose-reader/8723de00587d0979a2a3050e4883251aa439c73c/prose-react-native-demo/ios/prosereactnativedemo/Images.xcassets/SplashScreenLogo.imageset/image@3x.png -------------------------------------------------------------------------------- /prose-react-native-demo/ios/prosereactnativedemo/Supporting/Expo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EXUpdatesCheckOnLaunch 6 | ALWAYS 7 | EXUpdatesEnabled 8 | 9 | EXUpdatesLaunchWaitMs 10 | 0 11 | 12 | -------------------------------------------------------------------------------- /prose-react-native-demo/ios/prosereactnativedemo/prosereactnativedemo-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /prose-react-native-demo/ios/prosereactnativedemo/prosereactnativedemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /prose-react-native-demo/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require("expo/metro-config") 2 | 3 | const config = getDefaultConfig(__dirname) 4 | 5 | /** 6 | * This allow the web build .html to be loaded 7 | */ 8 | config.resolver.assetExts.push("html") 9 | 10 | /** 11 | * Shim required for now to be able to import @prose-reader/streamer 12 | */ 13 | config.resolver.resolveRequest = (context, moduleName, platform) => { 14 | if (moduleName === "string_decoder") { 15 | // Logic to resolve the module name to a file path... 16 | // NOTE: Throw an error if there is no resolution. 17 | return { 18 | filePath: "string_decoder.js", 19 | type: "sourceFile", 20 | } 21 | } 22 | // Optionally, chain to the standard Metro resolver. 23 | return context.resolveRequest(context, moduleName, platform) 24 | } 25 | 26 | module.exports = config 27 | -------------------------------------------------------------------------------- /prose-react-native-demo/string_decoder.js: -------------------------------------------------------------------------------- 1 | export const StringDecoder = () => {} 2 | -------------------------------------------------------------------------------- /prose-react-native-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": ["./*"] 7 | } 8 | }, 9 | "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /prose-react-native-demo/web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /prose-react-native-demo/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Prose Reader 7 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /prose-react-native-demo/web/src/style.css: -------------------------------------------------------------------------------- 1 | body, 2 | #reader, 3 | html { 4 | height: 100%; 5 | width: 100%; 6 | box-sizing: border-box; 7 | overflow: hidden; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | #reader { 13 | border: 2px solid red; 14 | } 15 | -------------------------------------------------------------------------------- /prose-react-native-demo/web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /prose-react-native-demo/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "es6", 5 | "lib": ["dom", "esnext"], 6 | "esModuleInterop": true, 7 | "useDefineForClassFields": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "bundler", 10 | "allowSyntheticDefaultImports": true, 11 | "noUncheckedIndexedAccess": true, 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /prose-react-native-demo/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path" 2 | import { defineConfig } from "vite" 3 | import { viteSingleFile } from "vite-plugin-singlefile" 4 | 5 | export default defineConfig({ 6 | build: { 7 | rollupOptions: { 8 | output: { 9 | dir: path.resolve(__dirname, "../assets"), 10 | }, 11 | }, 12 | }, 13 | optimizeDeps: { 14 | esbuildOptions: { 15 | // Node.js global to browser globalThis 16 | // fix sax on browser 17 | define: { 18 | global: "globalThis", 19 | }, 20 | }, 21 | }, 22 | plugins: [ 23 | viteSingleFile({ 24 | removeViteModuleLoader: true, 25 | }), 26 | ], 27 | }) 28 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": false, 7 | "moduleResolution": "bundler", 8 | "allowImportingTsExtensions": true, 9 | "isolatedModules": true, 10 | "moduleDetection": "force", 11 | "noEmit": true, 12 | "strict": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noUncheckedSideEffectImports": true, 17 | "noUncheckedIndexedAccess": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "module": "ESNext", 5 | "target": "es6", 6 | "lib": ["dom", "esnext"], 7 | "strict": true, 8 | "allowJs": false, 9 | "moduleResolution": "bundler", 10 | "allowSyntheticDefaultImports": true, 11 | "noUncheckedIndexedAccess": true, 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "declaration": true, 15 | "removeComments": true, 16 | "noEmit": true, 17 | "pretty": false, 18 | "stripInternal": true, 19 | "emitDecoratorMetadata": true, 20 | "experimentalDecorators": true, 21 | "sourceMap": true, 22 | "skipLibCheck": true, 23 | "resolveJsonModule": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true, 27 | "noUncheckedSideEffectImports": true, 28 | "moduleDetection": "force", 29 | "useDefineForClassFields": true 30 | }, 31 | "exclude": ["**/dist"] 32 | } 33 | --------------------------------------------------------------------------------