Files
silo-server/web/vendor/foliate-js
8b70357703 feat(ebooks): first-class ebook libraries, scanner, and reader (#124)
* docs: define ebook architecture matching audiobooks

* docs: plan ebook audiobook-parity implementation

* feat: add ebook scanner parser foundation

* fix: harden ebook scanner foundation

* fix: handle ebook isbn labels

* fix: guard ebook subtree scans

* feat: scan ebook libraries in core

* fix: preserve ebook scan people credits

* fix: refresh ebook scan metadata safely

* feat: persist ebook series membership

* test: cover ebook series persistence decisions

* fix: address ebook scanner PR review

* docs: clarify ebook foundation PR scope

* feat: add ebook metadata enricher

* fix: harden ebook poster cache

* feat: wire ebook metadata sync task

* feat: expose ebook library metadata setup

* feat: add ebook catalog scope support

* feat: add ebook detail view

* feat: label ebook file versions by format

* feat: use file-size copy for downloads

* feat: use file language in download dialog

* test: cover ebook detail authors and downloads

* fix: drop narrator credits from ebook scanner merges

* fix: align ebook collection filters with book media

* fix: drop asin provider ids from ebook enrichment

* fix: force ebook people refresh for stale narrators

* chore: omit ebook planning docs from branch

* feat: add ebook detail related content

* feat: add ebook reader file entrypoint

* feat: render ebooks with foliate reader

* feat: persist ebook reader progress

* feat: add ebook reader controls

* feat: extract ebook pdf metadata

* feat: favor scanner isbn during ebook enrichment

* feat: extract fbz ebook metadata

* feat: count cbz ebook pages

* feat: show ebook file page counts

* feat: show ebook download summaries

* feat: switch ebook reader files

* feat: prefer epub for ebook read action

* feat: surface ebook reader progress

* feat: sync ebook reader progress cache

* feat: hide ebook read action for unsupported files

* feat: filter ebook reader file selector

* fix: serve fbz ebook archives with reader mime type

* fix: detect fbz ebooks from compound filename

* fix: authorize fbz ebooks from compound filename

* fix: scope ebook catalog facets

* fix: reject narrator queries for ebooks

* fix: build ebook recommendation text from authors

* fix: include ebooks in embedding eligibility

* fix: include ebooks in recommendation media mix

* fix: include ebooks in recently added recommendations

* feat: include ebook progress in recommendation signals

* feat: include ebooks in continue watching sections

* feat: include ebooks in catalog progress metrics

* fix: read ebook isbn from epub metadata

* fix: filter ebook asin provider aliases

* fix: fall back from unsupported ebook reader files

* fix: sort ebook catalogs by reader progress

* fix: filter ebook catalogs by reader progress

* fix: include ebooks in last watched catalog filters

* feat: reflect ebook reader progress in item user state

* feat: share ebook progress state across item surfaces

* feat: report ebook scan progress

* fix: include ebook activity in recommendations

* fix: expose ebook reader progress on item detail

* fix: support ebook subtree scans

* fix: honor profile header for ebook item progress

* fix: add ebook library default sections

* fix: route ebook continue cards to reader

* fix: hide watched toggle for ebooks

* fix: route ebook watch tonight cards to reader

* fix: route ebook hero actions to reader

* fix: detect archive ebook reader formats by filename

* feat: cache embedded ebook covers during scan

* fix: encode ebook hero reader links

* fix: persist non-epub ebook reader progress

* fix: scope narrator catalog badges to audiobooks

* fix: merge ebook reader progress during item repair

* fix: label ebook progress filters as read

* fix: show ebook related rails as book covers

* fix: remove txt ebook reader support

* fix: reject txt ebook reader files

* fix: label ebook advanced filters as read

* fix: label ebook personalized sorts as read

* fix: remove plain text reader loader path

* test: cover ebook unread catalog rules

* fix: preserve ebook reader library context

* fix: link ebook genres with library scope

* fix: encode related rail item links

* fix: encode catalog card item links

* fix: encode hero and continue item links

* fix: encode watch tonight item links

* fix: encode recommendation and search item links

* test: cover ebook scan format set

* fix: label ebook search results clearly

* fix: make global search prompt media neutral

* fix: encode catalog read API ids

* fix: encode item API ids

* fix: include ebook reader vendor in docker build

* fix: make ebook reader build clean

* fix: clean ebook embedded descriptions

* docs: plan ebook reader shell parity

* feat: add ebook reader shell controls

* fix: widen ebook scrolled reader flow

* fix: remove scrolled reader content width cap

* docs: plan ebook reader full parity

* feat: persist ebook reader config

* feat: add ebook annotations and bookmarks

* feat: add ebook reader tools and aids

* feat: add ebook advanced reader settings

* fix: keep ebook reader panel in viewport

* fix: use foliate sizing units for ebook scroll flow

* fix: keep ebook settings controls readable

* fix: simplify ebook reader settings controls

* feat(ebooks): extract local covers during scan (#98)

* feat(ebooks): extract local covers during scan

* fix(ebooks): read nullable poster paths during cover scan

* fix(catalog): coalesce nullable media artwork fields

* fix(ebooks): group sibling formats by book identity

* fix(ebooks): tolerate legacy ebook metadata encodings

* fix(ebooks): decode PDF hex metadata strings

* fix(ebooks): harden local cover extraction and format grouping

Address review findings on the local cover scan:

- Restrict generic sidecar covers (cover.jpg, folder.png, ...) to
  single-book directories, always accept images named after the book
  file, and apply exactly one cover per reconcile with sidecar taking
  precedence over the embedded cover.
- Replace the read-then-write poster update with an atomic conditional
  UPDATE (ItemRepository.SetLocalPoster) so provider/admin artwork is
  never clobbered by concurrent writers, and refresh locally owned
  posters when the extracted cover bytes change (thumbhash compare).
- Preserve UTF-8 PDF Info strings (including a UTF-8 BOM) instead of
  forcing everything through Windows-1252; the cp1252 fallback now only
  applies to non-UTF-8 bytes.
- Select EPUB covers by manifest media-type with properties="cover-image"
  outranking the EPUB2 meta name="cover" id, so XHTML cover pages no
  longer shadow the real image.
- Order CBZ pages naturally (2.jpg before 10.jpg, ch2/ before ch10/)
  when picking the cover page, via a single O(n) min-scan.
- Bump the ebook content group key scheme to version 2 and reprocess
  rows written under older versions so pre-existing libraries gain
  sibling-format grouping instead of accumulating duplicates.
- Group different formats only (a same-format sibling with colliding
  sparse metadata stays a separate item) and stop a joining sibling's
  embedded metadata from overwriting a provider-matched item.
- Decode any IANA-labelled OPF/FB2 XML charset (windows-1251, koi8-r,
  shift_jis, ...) via x/net/html/charset, and wire the charset reader
  into FB2 parsing which previously had none.
- Strip the full .fb2.zip double extension from filename-derived titles
  and group keys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com>
Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(ebooks): add reader profiles and ruler (#99)

* feat(ebooks): extract local covers during scan

* fix(ebooks): read nullable poster paths during cover scan

* fix(catalog): coalesce nullable media artwork fields

* fix(ebooks): group sibling formats by book identity

* fix(ebooks): tolerate legacy ebook metadata encodings

* fix(ebooks): decode PDF hex metadata strings

* feat(ebooks): add reader profiles and ruler

* fix(ebooks): address reader ruler and profile review findings

- skip renderer setStyles/render when computed styles and attributes are
  unchanged, so ruler position updates no longer re-style the book view
- drag the ruler via a local draft that commits on release, with the
  surface rect cached at pointer-down
- migrate font values persisted before the generic stacks (Inter,
  Georgia, Merriweather, legacy serif) so the font select never renders
  blank, with a Custom fallback option for unknown values
- make the ruler band click-through and move dragging to a dedicated
  keyboard-accessible slider handle so links and text selection keep
  working under the band
- share font stacks between options and profiles via READER_FONT_STACKS
- surface the active reading profile, move presets to the top of the
  settings panel, and drop the redundant profile button aria-labels

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ebooks): resolve prefer-const lint error in readest document lib

`pnpm run lint` failed on the branch because `direction` is never
reassigned in getDirection; split the destructure so only the
reassigned `writingMode` stays mutable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com>
Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Merge branch 'main' into work/ebooks-reader-base

Brings the ebook integration branch up to date with main (audiobook
library redesign, continue-watching rework and card affordances,
quic-go bump, jellycompat fixes). Conflict resolutions favor main's
generalized mechanisms and register ebooks with them:

- media scope validation goes through IsValidMediaScope (now including
  "ebook" alongside main's "video" group scope), in Go and in the web
  filter/search types
- continue-watching uses main's typed rails; reading-type sections pull
  resume points from ebook_reader_progress and the ebook library default
  section is wired to ContinueTypeConfig(ContinueTypeReading)
- item_repo keeps main's derived select-list machinery (itemColumnExpr)
  and both poster accessors (GetPoster/SetLocalPoster for ebook covers,
  GetPosterPath for audiobook covers)
- web cards/hero/watch-tonight adopt main's buildMediaPlayHref helpers,
  which now route ebooks to /reader/ebook and encode content ids;
  ebook affordances (BookOpen icon, Read verb, percent-read subtitle)
  carry over onto main's reworked components
- LibraryForm ebook support ported into main's refactored
  useLibraryForm/libraryTypes modules

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(docker): copy foliate-js vendor into Dockerfile.dev frontend stage

foliate-js is a file:vendor/foliate-js dependency, so pnpm install needs
the vendor directory before the lockfile install layer. The production
Dockerfile already copies it; the dev image was missed, breaking
make dev-deploy with ENOENT on /app/web/vendor/foliate-js.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ebooks): render Continue Reading sections as upright poster cards

All-ebook continue sections previously fell through to the horizontal
16:9 wide card; include ebooks in the poster-variant check so book
covers render in their natural 2:3 framing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): stop related-rail highlight ring clipping on detail pages

Move the current-item ring onto the cover artwork with a themed
ring-offset color (matching the sidebar profile highlight) and give
the scroll container top headroom so the ring is not cut off by
overflow-x-auto. Applies to both ebook and audiobook detail rails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(scanner): harden ebook scanning against data loss and bad metadata

- Reconcile missing ebook files like video/audio, with real per-root walk
  failure tracking (failed/unmounted roots are excluded from deletion),
  symlinked-root support via the shared logical walker, and the empty-root
  cleanup allowance before any destructive reconciliation.
- Create ebook items as 'pending' so enrichment can promote them to
  'matched' (backfill migration included), and protect matched items from
  re-scan clobbering: title/year skipped, people/series fill-empty only.
- PDF metadata: scan head + tail windows (non-linearized PDFs keep the Info
  dict at the end), require proper key delimiters, head values win.
- Cap plain .fb2 reads like .fbz entries; drop .md as an ebook format.
- gofmt internal/scanner/audiobook.go (pre-existing drift).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ebooks): make enrichment failures non-terminal with dedicated backoff state

- Provider errors now record a failure (capped retries) instead of stamping
  last_refreshed, which permanently excluded items after transient outages.
- Unconfigured metadata chains and the scan-window membership race skip the
  item without stamping or burning a retry.
- Failure tracking moves to a new ebook_enrichment_state table, decoupling
  it from media_items.refresh_failures (shared with metadata refresh debt).
- Preserve non-author people credits when persisting enrichment results.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(catalog): gate ebook progress on hidden history and centralize threshold

- Apply user_history_hidden_items gating (video semantics) to the ebook
  watched/in-progress filters, progress sort plan, and Continue Reading.
- Continue Reading pages past dismissed items via the shared collector and
  dedupes items across pages (also fixes the video path's latent exposure).
- Centralize the 0.9 finished threshold as models.EbookFinishedProgressThreshold
  with a single SQL-interpolated mirror in catalog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(recommendations): correct watcher counting and wire ebook taste signals

- itemWatchersQuery dedupes to distinct (watcher, item) rows so one
  binge-watcher can no longer satisfy minWatchers; the eligibility floor
  now counts distinct accounts rather than profiles.
- Hidden-history gating on GetEbookReaderProgressForUser (signal reader).
- Ebook reading produces canonical implicit taste signals (weighted like
  the equivalent movie progress ratio); ebooks join taste-seed candidates.
- Stale GetRecentlyAddedItems doc comment corrected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(api): harden ebook reader endpoints and serve a Content-Security-Policy

- Serve a CSP on all SPA HTML responses: blob/srcdoc book iframes inherit
  it, so script-src 'self' 'wasm-unsafe-eval' blocks script execution from
  malicious book content (sandbox alone is defeated by the WebKit
  allow-scripts requirement). Threat model documented on the constant.
- X-Content-Type-Options: nosniff on frontend, jellycompat, and ebook file
  responses; MIME resolution can no longer fall through to octet-stream
  for an admitted ebook file.
- Annotation PATCH: presence-aware field semantics (absent keeps, present
  sets/clears), invariant re-validation on the merged row, and an atomic
  SELECT ... FOR UPDATE read-merge-write.
- Request size caps (413) on progress/config/annotation writes;
  Content-Disposition via mime.FormatMediaType; hidden-history gating in
  the shared ebook progress lister; FK-cascade indexes for reader tables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(api): native read-state endpoints for ebooks

- POST/DELETE /watched/{id} accepts ebook content IDs: mark read upserts
  progress 1.0 preserving the reader's file/location (or picks the
  preferred reader file for never-opened books); mark unread mirrors video
  unwatch semantics and deletes the progress row.
- /history/remove accepts ebooks: hides via user_history_hidden_items
  without touching the reading position (hidden != unread; next reading
  activity resurfaces the book, mirroring video re-watch).
- Access-filter checks match the video branch; shared logic lives in
  ebook_read_state.go. Sort metrics/user-state thresholds use the shared
  constant; profile-header fallback deduplicated.

Clients: response is {type: "ebook", affected_count: 1, played: bool};
the existing watched SSE event fires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): harden the ebook reader UI

- Open-flow race: cancellation checked after every await with full stale-run
  teardown (no wrong-file progress saves, no leaked views/blob URLs);
  book.destroy() on cleanup.
- Progress: monotonic stale-response guard; visibilitychange flush uses the
  refresh-capable client, pagehide uses keepalive; per-book cross-format
  progress documented as deliberate.
- Settings: side effects out of the setState updater; local edits no longer
  clobbered by late server config; pending saves flushed on unmount/pagehide.
- TTS: generation token so Stop actually stops (Chromium/Firefox synthetic
  events); Media Session uninstalled on unmount.
- External book links: http(s) only, opened with noopener,noreferrer.
- apiBlob 512 MiB guard with a user-facing error; fraction bookmarks
  navigable; search-result key collisions fixed; dead e-ink code removed;
  getLibrarySortRelevanceScope deduplicated; md format dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): mark read/unread affordances for ebooks

- Item detail gets a Mark Read/Unread button; card menus drop the ebook
  gate and share type-aware labels/toasts (also dedupes audiobook wording).
- Watched-state invalidation includes the reader progress query key so the
  Continue button and percent refresh after toggling.
- Continue Reading dismiss copy for ebooks; dismissal path now URL-encodes
  item IDs (ebook content IDs can contain reserved characters).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: record the PR #124 review and hardening pass

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 08:18:35 -04:00
..

foliate-js

Library for rendering e-books in the browser.

Features:

  • Supports EPUB, MOBI, KF8 (AZW3), FB2, CBZ, PDF (experimental; requires PDF.js)
  • Add support for other formats yourself by implementing the book interface
  • Pure JavaScript
  • Small and modular
  • No hard dependencies
  • Does not require loading whole file into memory
  • Does not care about older browsers

Demo

The repo includes a demo viewer that can be used to open local files. To use it, serve the files with a server, and navigate to reader.html. Or visit the online demo hosted on GitHub. Note that it is very incomplete at the moment, and lacks many basic features such as keyboard shortcuts.

Also note that deobfuscating fonts with the IDPF algorithm requires a SHA-1 function. By default it uses Web Crypto, which is only available in secure contexts. Without HTTPS, you will need to modify reader.js and pass your own SHA-1 implementation.

Current Status

It works reasonably well, and has been used in several stable releases of Foliate. This library itself is, however, not stable. Expect it to break and the API to change at any time. Use it at your own risk.

If you do decide to use it, since there's no release yet, it is recommended that you include the library as a git submodule in your project so that you can easily update it.

Documentation

Overview

This project uses native ES modules. There's no build step, and you can import them directly.

There are mainly three kinds of modules:

  • Modules that parse and load books, implementing the "book" interface
    • comic-book.js, for comic book archives (CBZ)
    • epub.js and epubcfi.js, for EPUB
    • fb2.js, for FictionBook 2
    • mobi.js, for both Mobipocket files and KF8 (commonly known as AZW3) files
  • Modules that handle pagination, implementing the "renderer" interface
    • fixed-layout.js, for fixed layout books
    • paginator.js, for reflowable books
  • Auxiliary modules used to add additional functionalities
    • overlayer.js, for rendering annotations
    • progress.js, for getting reading progress
    • search.js, for searching

The modules are designed to be modular. In general, they don't directly depend on each other. Instead they depend on certain interfaces, detailed below. The exception is view.js. It is the higher level renderer that strings most of the things together, and you can think of it as the main entry point of the library. See "Basic Usage" below.

The repo also includes a still higher level reader, though strictly speaking, reader.html (along with reader.js and its associated files in ui/) is not considered part of the library itself. It's akin to Epub.js Reader. You are expected to modify it or replace it with your own code.

Basic Usage

To get started, clone the repo and import view.js:

import './foliate-js/view.js'

const view = document.createElement('foliate-view')
document.body.append(view)

view.addEventListener('relocate', e => {
    console.log('location changed')
    console.log(e.detail)
})

// can open a File/Blob object or a URL
// or any object that implements the "book" interface
await view.open('example.epub')
await view.goTo(/* path, section index, or CFI */)

See the online demo for a more advanced example.

Security

EPUB books can contain scripted content (i.e. JavaScript in the e-book), which is potentially dangerous, and is not supported by this library because

  • It is currently impossible to do so securely due to the content being served from the same origin (using blob: URLs).
  • Due to WebKit Bug 218086, the allow-scripts attribute is required on iframes, which renders iframe sandbox useless.

It is therefore imperative that you use Content Security Policy (CSP) to block all scripts except 'self'. An EPUB file for testing can be found at https://github.com/johnfactotum/epub-test.

Caution

Do NOT use this library (or any other e-book library, for that matter) without CSP unless you completely trust the content you're rendering or can block scripts by other means.

The Main Interface for Books

Processors for each book format return an object that implements the following interface:

  • .sections: an array of sections in the book. Each item has the following properties:
    • .load(): returns a string containing the URL that will be rendered. May be async.
    • .unload(): returns nothing. If present, can be used to free the section.
    • .createDocument(): returns a Document object of the section. Used for searching. May be async.
    • .size: a number, the byte size of the section. Used for showing reading progress.
    • .linear: a string. If it is "no", the section is not part of the linear reading sequence (see the linear attribute in EPUB).
    • .cfi: base CFI string of the section. The part that goes before the ! in CFIs.
    • .id: an identifier for the section, used for getting TOC item (see below). Can be anything, as long as they can be used as keys in a Map.
  • .dir: a string representing the page progression direction of the book ("rtl" or "ltr").
  • .toc: an array representing the table of contents of the book. Each item has
    • .label: a string label for the item
    • .href: a string representing the destination of the item. Does not have to be a valid URL.
    • .subitems: a array that contains TOC items
  • .pageList: same as the TOC, but for the page list.
  • .metadata: an object representing the metadata of the book. Currently, it follows more or less the metadata schema of Readium's webpub manifest. Note that titles and names can be a string or an object like { ja: "草枕", en: 'Kusamakura' }, and authors, etc. can be a string, an object, or an array of strings or objects.
  • .rendition: an object that contains properties that correspond to the rendition properties in EPUB. If .layout is "pre-paginated", the book is rendered with the fixed layout renderer.
  • .resolveHref(href): given an href string, returns an object representing the destination referenced by the href, which has the following properties:
    • .index: the index of the referenced section in the .section array
    • .anchor(doc): given a Document object, returns the document fragment referred to by the href (can either be an Element or a Range), or null
  • .resolveCFI(cfi): same as above, but with a CFI string instead of href
  • .isExternal(href): returns a boolean. If true, the link should be opened externally.

The following methods are consumed by progress.js, for getting the correct TOC and page list item when navigating:

  • .splitTOCHref(href): given an href string (from the TOC), returns an array, the first element of which is the id of the section (see above), and the second element is the fragment identifier (can be any type; see below). May be async.
  • .getTOCFragment(doc, id): given a Document object and a fragment identifier (the one provided by .splitTOCHref(); see above), returns a Node representing the target linked by the TOC item

In addition, the .transformTarget, if present, can be used to transform the contents of the book as it loads. It is an EventTarget with a custom event "data", whose .detail is { data, type, name }, where .data is either a string or Blob, or a Promise thereof, .type the content type string, and .name the identifier of the resource. Event handlers should mutate .data to transform the data.

Almost all of the properties and methods are optional. At minimum it needs .sections and the .load() method for the sections, as otherwise there won't be anything to render.

Archived Files

Reading Zip-based formats requires adapting an external library. Both epub.js and comic-book.js expect a loader object that implements the following interface:

  • .entries: (only used by comic-book.js) an array, each element of which has a filename property, which is a string containing the filename (the full path).
  • .loadText(filename): given the path, returns the contents of the file as string. May be async.
  • .loadBlob(filename): given the path, returns the file as a Blob object. May be async.
  • .getSize(filename): returns the file size in bytes. Used to set the .size property for .sections (see above).

In view.js, this is implemented using zip.js, which is highly recommended because it seems to be the only library that supports random access for File objects (as well as HTTP range requests).

One advantage of having such an interface is that one can easily use it for reading unarchived files as well. For example, view.js has a loader that allows you to open unpacked EPUBs as directories.

Mobipocket and Kindle Files

It can read both MOBI and KF8 (.azw3, and combo .mobi files) from a File (or Blob) object. For MOBI files, it decompresses all text at once and splits the raw markup into sections at every <mbp:pagebreak>, instead of outputting one long page for the whole book, which drastically improves rendering performance. For KF8 files, it tries to decompress as little text as possible when loading a section, but it can still be quite slow due to the slowness of the current HUFF/CDIC decompressor implementation. In all cases, images and other resources are not loaded until they are needed.

Note that KF8 files can contain fonts that are zlib-compressed. They need to be decompressed with an external library. view.js uses fflate to decompress them.

PDF and Other Fixed-Layout Formats

There is a proof-of-concept, highly experimental adapter for PDF.js, with which you can show PDFs using the same fixed-layout renderer for EPUBs.

CBZs are similarly handled like fixed-layout EPUBs.

The Renderers

To simplify things, it has two separate renderers, one for reflowable books, and one for fixed layout books (as such there's no support for mixed layout books). These renderers are custom elements (web components).

A renderer's interface is currently mainly:

  • .open(book): open a book object.
  • .goTo({ index, anchor }): navigate to a destination. The argument has the same type as the one returned by .resolveHref() in the book object.
  • .prev(): go to previous page.
  • .next(): go to next page.

It has the following custom events:

  • load, when a section is loaded. Its event.detail has two properties, doc, the Document object, and index, the index of the section.
  • relocate, when the location changes. Its event.detail has the properties range, index, and fraction, where range is a Range object containing the current visible area, and fraction is a number between 0 and 1, representing the reading progress within the section.
  • create-overlayer, which allows adding an overlay to the page. The event.detail has the properties doc, index, and a function attach(overlay), which should be called with an overlayer object (see the description for overlayer.js below).

Both renderers have the part named filter, which you can apply CSS filters to, to e.g. invert colors or adjust brightness:

foliate-view::part(filter) {
    filter: invert(1) hue-rotate(180deg);
}

The filter only applies to the book itself, leaving overlaid elements such as highlights unaffected.

The Paginator

The paginator uses the same pagination strategy as Epub.js: it uses CSS multi-column. As such it shares much of the same limitations (it's slow, some CSS styles do not work as expected, and other bugs). There are a few differences:

  • It is a totally standalone module. You can use it to paginate any content.
  • It is much simpler, but currently there's no support for continuous scrolling.
  • It has no concept of CFIs and operates on Range objects directly.
  • It uses bisecting to find the current visible range, which is more accurate than what Epub.js does.
  • It has an internal #anchor property, which can be a Range, Element, or a fraction that represents the current location. The view is anchored to it no matter how you resize the window.
  • It supports more than two columns.
  • It supports switching between scrolled and paginated mode without reloading (I can't figure out how to do this in Epub.js).

The layout can be configured by setting the following attributes:

  • animated: a boolean attribute. If present, adds a sliding transition effect.
  • flow: either paginated or scrolled.
  • margin: a CSS <length>. The unit must be px. The height of the header and footer.
  • gap: a CSS <percentage>. The size of the space between columns, relative to page size.
  • max-inline-size: a CSS <length>. The unit must be px. The maximum inline size of the text (column width in paginated mode).
  • max-block-size: same as above, but for the size in the block direction.
  • max-column-count: integer. The maximum number of columns. Has no effect in scrolled mode, or when the orientation of the renderer element is portrait (or, for vertical writing, landscape).

(Note: there's no JS property API. You must use .setAttribute().)

It has built-in header and footer regions accessible via the .heads and .feet properties of the paginator instance. These can be used to display running heads and reading progress. They are only available in paginated mode, and there will be one element for each column. They are styleable with ::part(head) and ::part(foot). E.g., to add a border under the running heads,

foliate-view::part(head) {
    padding-bottom: 4px;
    border-bottom: 1px solid graytext;
}

EPUB CFI

Parsed CFIs are represented as a plain array or object. The basic type is called a "part", which is an object with the following structure: { index, id, offset, temporal, spatial, text, side }, corresponding to a step + offset in the CFI.

A collapsed, non-range CFI is represented as an array whose elements are arrays of parts, each corresponding to a full path. That is, /6/4!/4 is turned into

[
    [
        { "index": 6 },
        { "index": 4 }
    ],
    [
        { "index": 4 }
    ]
]

A range CFI is an object { parent, start, end }, each property being the same type as a collapsed CFI. For example, /6/4!/2,/2,/4 is represented as

{
    "parent": [
        [
            { "index": 6 },
            { "index": 4 }
        ],
        [
            { "index": 2 }
        ]
    ],
    "start": [
        [
            { "index": 2 }
        ]
    ],
    "end": [
        [
            { "index": 4 }
        ]
    ]
}

The parser uses a state machine rather than regex, and should handle assertions that contain escaped characters correctly (see tests for examples of this).

It has the ability ignore nodes, which is needed if you want to inject your own nodes into the document without affecting CFIs. To do this, you need to pass the optional filter function that works similarly to the filter function of TreeWalkers:

const filter = node => node.nodeType !== 1 ? NodeFilter.FILTER_ACCEPT
    : node.matches('.reject') ? NodeFilter.FILTER_REJECT
    : node.matches('.skip') ? NodeFilter.FILTER_SKIP
    : NodeFilter.FILTER_ACCEPT

CFI.toRange(doc, 'epubcfi(...)', filter)
CFI.fromRange(range, filter)

It can parse and stringify spatial and temporal offsets, as well as text location assertions and side bias, but there's no support for employing them when rendering yet.

Highlighting Text

There is a generic module for overlaying arbitrary SVG elements, overlayer.js. It can be used to implement highlighting text for annotations. It's the same technique used by marks-pane, used by Epub.js, but it's designed to be easily extensible. You can return any SVG element in the draw function, making it possible to add custom styles such as squiggly lines or even free hand drawings.

The overlay has no event listeners by default. It only provides a .hitTest(event) method, that can be used to do hit tests. Currently it does this with the client rects of Ranges, not the element returned by draw().

An overlayer object implements the following interface for the consumption of renderers:

  • .element: the DOM element of the overlayer. This element will be inserted, resized, and positioned automatically by the renderer on top of the page.
  • .redraw(): called by the renderer when the overlay needs to be redrawn.

The Text Walker

Not a particularly descriptive name, but essentially, text-walker.js is a small DOM utility that allows you to

  1. Gather all text nodes in a Range, Document or DocumentFragment into an array of strings.
  2. Perform splitting or matching on the strings.
  3. Get back the results of these string operations as Ranges.

E.g. you can join all the text nodes together, use Intl.Segmenter to segment the string into words, and get the results in DOM Ranges, so you can mark up those words in the original document.

In foliate-js, this is used for searching and TTS.

Searching

It provides a search module, which can in fact be used as a standalone module for searching across any array of strings. There's no limit on the number of strings a match is allowed to span. It's based on Intl.Collator and Intl.Segmenter, to support ignoring diacritics and matching whole words only. It's extrenely slow, and you'd probably want to load results incrementally.

Text-to-Speech (TTS)

The TTS module doesn't directly handle speech output. Rather, its methods return SSML documents (as strings), which you can then feed to your speech synthesizer.

The SSML attributes ssml:ph and ssml:alphabet are supported. There's no support for PLS and CSS Speech.

Offline Dictionaries

The dict.js module can be used to load dictd and StarDict dictionaries. Usage:

import { StarDict } from './dict.js'
import { inflate } from 'your inflate implementation'

const { ifo, dz, idx, syn } = { /* `File` (or `Blob`) objects */ }
const dict = new StarDict()
await dict.loadIfo(ifo)
await dict.loadDict(dz, inflate)
await dict.loadIdx(idx)
await dict.loadSyn(syn)

// look up words
const query = '...'
await dictionary.lookup(query)
await dictionary.synonyms(query)

Note that you must supply your own inflate function. Here is an example using fflate:

const inflate = data => new Promise(resolve => {
    const inflate = new fflate.Inflate()
    inflate.ondata = data => resolve(data)
    inflate.push(data)
})

OPDS

The opds.js module can be used to implement OPDS clients. It can convert OPDS 1.x documents to OPDS 2.0:

  • getFeed(doc): converts an OPDS 1.x feed to OPDS 2.0. The argument must be a DOM Document object. You need to use a DOMParser to obtain a Document first if you have a string.
  • getPublication(entry): converts a OPDS 1.x entry in acquisition feeds to an OPDS 2.0 publication. The argument must be a DOM Element object.

It exports the following symbols for properties unsupported by OPDS 2.0:

  • SYMBOL.SUMMARY: used on navigation links to represent the summary/content (see https://github.com/opds-community/drafts/issues/51)
  • SYMBOL.CONTENT: used on publications to represent the content/description and its type. This is mainly for preserving the type info for XHTML. The value of this property is an object whose properties are:
    • .type: either "text", "html", or "xhtml"
    • .value: the value of the content

There are also two functions that can be used to implement search forms:

  • getOpenSearch(doc): for OpenSearch. The argument is a DOM Document object of an OpenSearch search document.
  • getSearch(link) for templated search in OPDS 2.0. The argument must be an OPDS 2.0 Link object. Note that this function will import uri-template.js.

These two functions return an object that implements the following interface:

  • .metadata: an object with the string properties title and description
  • .params: an array, representing the search parameters, whose elements are objects whose properties are
    • ns: a string; the namespace of the parameter
    • name: a string; the name of the parameter
    • required: a boolean, whether the parameter is required
    • value: a string; the default value of the parameter
  • .search(map): a function, whose argument is a Map whose values are Maps (i.e. a two-dimensional map). The first key is the namespace of the search parameter. For non-namespaced parameters, the first key must be null. The second key is the parameter's name. Returns a string representing the URL of the search results.

Generating Images for Quotes

With quote-image.js, one can generate shareable images for quotes:

document.querySelector('foliate-quoteimage').getBlob({
    title: 'The Time Machine',
    author: 'H. G. Wells',
    text: 'Can an instantaneous cube exist?',
})

Supported Browsers

It aims to support the latest version of WebKitGTK, Firefox, and Chromium. Older browsers like Firefox ESR are not supported.

Although it's mainly indeded for rendering e-books in the browser, some features can be used in non-browser environments as well. In particular, epubcfi.js can be used as is in any environment if you only need to parse or sort CFIs. Most other features depend on having the global objects Blob, TextDecoder, TextEncoder, DOMParser, XMLSerializer, and URL, and should work if you polyfill them.

License

MIT.

Vendored libraries:

  • zip.js is licensed under the BSD-3-Clause license.
  • fflate is MIT licensed.
  • PDF.js is licensed under Apache.