├── .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 |
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 |
16 | {children}
17 |
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 |
--------------------------------------------------------------------------------