docs: document the foliate-js reader

Adds a reader section covering the vendored tree, the $foliate alias and the
traps around it, and refreshes the stale stack line, oklch claim and rough
edges. Records why CSP is not enabled in TODO.md.
This commit is contained in:
2026-08-12 01:37:19 -04:00
parent 961a63480e
commit 51c31e6bf6
3 changed files with 106 additions and 23 deletions
+9 -7
View File
@@ -9,13 +9,15 @@ a KOSync-compatible endpoint.
## Layout ## Layout
| Path | What | | Path | What |
| --- | --- | | -------------------------- | ---------------------------------------------------------------------------------------- |
| `backend/` | Litestar REST API + PostgreSQL. See `backend/AGENTS.md`. | | `backend/` | Litestar REST API + PostgreSQL. See `backend/AGENTS.md`. |
| `frontend/` | SvelteKit SSR web app. See `frontend/AGENTS.md`. | | `frontend/` | SvelteKit SSR web app. See `frontend/AGENTS.md`. |
| `docker-compose.yml` | Production stack: `db` (postgres:17), `backend`, `frontend`. | | `frontend/src/lib/vendor/` | Vendored `foliate-js` (the EPUB engine), copied by `frontend/scripts/vendor-foliate.sh`. |
| `docs/screenshots/` | Images used by `README.md`. | | `frontend/static/pdfjs/` | Vendored pdf.js viewer, used by the PDF reader in an iframe. |
| `shell.nix` | Root dev shell; composes the two sub-shells. | | `docker-compose.yml` | Production stack: `db` (postgres:17), `backend`, `frontend`. |
| `docs/screenshots/` | Images used by `README.md`. |
| `shell.nix` | Root dev shell; composes the two sub-shells. |
## Development environment ## Development environment
+23
View File
@@ -51,6 +51,29 @@ Worth adding at the same time:
## Frontend ## Frontend
### No CSP, so scripted EPUBs run against the app origin
EPUB files may contain JavaScript. foliate-js renders each section in an iframe from a
**same-origin** `blob:` URL and cannot sandbox it — `allow-scripts` is required, and
blob URLs inherit the embedder's origin — so script inside a book can reach `/api/*`
with the session cookie attached. foliate's own README says not to use it without a
Content Security Policy blocking scripts.
The obvious policy is `kit.csp` in `svelte.config.js` with `script-src: ['self']`, and
deliberately no `default-src` (it would also cover `style-src`/`img-src`/`font-src` and
kill both the book's own blob: assets and the inline `<style id="chitai-theme">` that
`hooks.server.ts` injects via `transformPageChunk`).
**What blocks it:** `mode-watcher` renders its own inline `setInitialMode` script, which
sets the dark class before first paint. SvelteKit only nonces the bootstrap script it
injects itself, so that one is blocked and every page load flashes the light theme.
Fixing it means pinning a SHA-256 of a third-party inline script whose contents change
with the package version and the props passed — it would break silently on upgrade, and
the symptom would be a theme flash rather than an error.
Worth revisiting if `mode-watcher` gains a nonce prop, or if the theme class moves to a
cookie so the server can set it without an inline script.
### Remove the epub.js locations-cache purge ### Remove the epub.js locations-cache purge
`frontend/src/lib/reader/legacy-cache.ts``purgeLegacyLocationCache` `frontend/src/lib/reader/legacy-cache.ts``purgeLegacyLocationCache`
+74 -16
View File
@@ -4,13 +4,16 @@ SvelteKit web app for the eBook library. See the repo-root `AGENTS.md` for the o
dev-environment setup. dev-environment setup.
**Stack:** SvelteKit 2 with `adapter-node` · Svelte 5 (runes) · Tailwind v4 · Zod v4 · **Stack:** SvelteKit 2 with `adapter-node` · Svelte 5 (runes) · Tailwind v4 · Zod v4 ·
`epubjs` · `mode-watcher` (dark mode) · `svelte-sonner` (toasts) · pnpm. vendored `foliate-js` (EPUB) · vendored `pdf.js` (PDF) · `mode-watcher` (dark mode) ·
`svelte-sonner` (toasts) · pnpm.
Two experimental flags are on in `svelte.config.js` and the codebase depends on both: Two experimental flags are on in `svelte.config.js` and the codebase depends on both:
`kit.experimental.remoteFunctions` and `compilerOptions.experimental.async` (`await` in components). `kit.experimental.remoteFunctions` and `compilerOptions.experimental.async` (`await` in components).
Tailwind v4 has **no config file** — the theme, oklch colour tokens and `@custom-variant dark` all Tailwind v4 has **no config file** — the theme, colour tokens (hex, not oklch) and
live in `src/app.css`. `@custom-variant dark` all live in `src/app.css`. `dark` is the _only_ custom variant defined, so
generated components that assume others — shadcn's slider ships `data-horizontal:` / `data-vertical:`
classes — silently produce no styles. Use the `data-[orientation=…]` form instead.
## Talking to the backend ## Talking to the backend
@@ -65,8 +68,12 @@ Svelte context with a module-level `Symbol` key and a `setXState` / `getXState`
```ts ```ts
const LIBRARY_KEY = Symbol('LIBRARY'); const LIBRARY_KEY = Symbol('LIBRARY');
export function setLibraryState(libraries: Library[]) { return setContext(LIBRARY_KEY, new LibraryState(libraries)); } export function setLibraryState(libraries: Library[]) {
export function getLibraryState() { return getContext<ReturnType<typeof setLibraryState>>(LIBRARY_KEY); } return setContext(LIBRARY_KEY, new LibraryState(libraries));
}
export function getLibraryState() {
return getContext<ReturnType<typeof setLibraryState>>(LIBRARY_KEY);
}
``` ```
Follow that pattern rather than introducing stores. `library.svelte.ts` is the reference — including Follow that pattern rather than introducing stores. `library.svelte.ts` is the reference — including
@@ -79,10 +86,56 @@ its optimistic-delete-with-rollback and toast handling. `bookCollection` / `book
`@ieedan/shadcn-svelte-extras` (`jsrepo.json`). Treat as generated: add components with the CLIs `@ieedan/shadcn-svelte-extras` (`jsrepo.json`). Treat as generated: add components with the CLIs
rather than hand-writing them, and prefer wrapping over editing. rather than hand-writing them, and prefer wrapping over editing.
- App components live in `forms/`, `layout/`, `view/` (browser, grid/list/table, filters, sort) and - App components live in `forms/`, `layout/`, `view/` (browser, grid/list/table, filters, sort) and
`reader/` (epub reader + chapter sidebar). `reader/` (see [The readers](#the-readers)).
- `cn()` from `$lib/utils` merges Tailwind classes; the `WithElementRef` / `WithoutChild` helpers - `cn()` from `$lib/utils` merges Tailwind classes; the `WithElementRef` / `WithoutChild` helpers
there are the shadcn prop-typing conventions. there are the shadcn prop-typing conventions.
## The readers
**PDF** is the pdf.js viewer vendored under `static/pdfjs/`, pointed at by an iframe. Untouched by
the EPUB work; leave it alone unless the task is about PDFs.
**EPUB** is built on `foliate-js`, copied verbatim into `src/lib/vendor/foliate-js/` by
`scripts/vendor-foliate.sh` (pinned commit; see `src/lib/vendor/foliate-js/README.chitai.md`).
Upstream has no npm release and recommends a submodule; this repo has none and already vendors
pdf.js the same way, so it is copied instead. Only the import closure reachable from `view.js` is
vendored, and **`pdf.js` in that directory is our stub, not upstream's** — the real one imports a
bare `@pdfjs/pdf.min.mjs` that Rollup resolves at build time even though the path never runs.
Layout:
| Path | What |
| ------------------------------------------ | ---------------------------------------------------------------------------- |
| `lib/vendor/foliate-js/` | The engine. Do not edit — `vendor-foliate.sh` overwrites it. |
| `lib/reader/foliate.ts` | Lazy loader for the custom elements. The only thing that imports `$foliate`. |
| `lib/reader/settings.ts` · `stylesheet.ts` | Defaults/bounds, and the CSS injected into the book. |
| `lib/reader/progress.ts` | Debounced progress writer with a `sendBeacon` flush. |
| `lib/state/reader-settings.svelte.ts` | Settings state, persisted to `localStorage`. |
| `components/reader/foliate-view.svelte` | Wraps `<foliate-view>`; owns the imperative lifecycle. |
| `components/reader/epub-reader.svelte` | The shell: chrome, TOC, errors, progress. |
Things that will bite:
- **`$foliate` is a Vite-only alias.** It is deliberately absent from `kit.alias` and tsconfig
`paths` so TypeScript cannot resolve it and falls back to the ambient declaration in
`lib/reader/foliate-js.d.ts`; `src/lib/vendor` is also in tsconfig `exclude`. Without both,
`checkJs` walks ~11k lines of untyped JS. The declaration file must **not** be named `foliate.d.ts`
— beside `foliate.ts`, TypeScript takes it for that file's emitted declaration and drops it.
- **Never import the vendored code at module scope.** `view.js` calls `customElements.define` and
subclasses `HTMLElement` on import, so it must stay behind `loadFoliate()` inside `onMount`. SSR is
otherwise on for the reader route.
- **Sections render in iframes, which swallow key events.** Keyboard handlers are bound per section
document on the `load` event, and modifier combinations are replayed onto the host window so app
shortcuts (the sidebar's ctrl+B) still work while reading.
- **Renderer settings split two ways.** Flow, gap, margins, column count and line width are
_attributes_ set with `setAttribute` (there is no JS property API, no `margin` shorthand and no
`spread` — a spread is `max-column-count: 2`). Typography is CSS passed to `renderer.setStyles`,
which takes a `[before, after]` pair: the first is prepended to the section head so the book
overrides it, the second appended so it wins. User settings belong in the second, with
`!important`, or the book's own CSS beats them.
- **Progress needs no locations pre-pass.** `relocate` carries both a CFI and an overall `fraction`,
which map straight onto `epub_cfi` and `percentage`.
## Routing ## Routing
Route groups carry the layout structure: Route groups carry the layout structure:
@@ -95,21 +148,26 @@ Route groups carry the layout structure:
## Conventions ## Conventions
Prettier (`.prettierrc`): tabs, single quotes, no trailing commas, 100 columns, with the Svelte and Prettier (`.prettierrc`): tabs, single quotes, no trailing commas, 100 columns, with the Svelte and
Tailwind plugins. Run `pnpm check` (svelte-check) and `pnpm lint` before considering work done. Tailwind plugins. Run `pnpm check` (svelte-check) and `pnpm lint` before considering work done
but take a baseline first, because neither is clean (see below).
`src/lib/vendor/` is excluded from Prettier, ESLint and svelte-check. Don't reformat vendored code.
## Known rough edges ## Known rough edges
Observed in the current tree — don't mistake these for intentional patterns to copy: Observed in the current tree — don't mistake these for intentional patterns to copy:
- `src/lib/schema/openapi/schema.d.ts` is **stale** — it predates the `BookProgress` rework and has - `src/lib/schema/openapi/schema.d.ts` is **stale** — it predates the `BookProgress` rework and has
no `BookProgressRead`, so `book.progress` types as `{}`. This is the source of most of the ~104 no `BookProgressRead`, so `book.progress` types as `{}`. It is the source of most of the errors
errors `pnpm check` reports on a clean tree; regenerating it should clear them. Get a baseline `pnpm check` reports; regenerating it should clear them. **Baseline as of 2026-08-11: 37 errors,
before assuming an error is yours. 1 warning, 11 files.** Get your own baseline before assuming an error is yours.
- `pnpm lint` does not pass either — 131 pre-existing ESLint errors, 35 of them
`svelte/no-navigation-without-resolve` on plain `href`s, plus ~59 files Prettier would rewrite
(mostly vendored shadcn components). Check the files you touched, not the whole tree.
- `src/routes/api/[...path]/+server.ts` — all four handlers are annotated `RequestHandler` while the - `src/routes/api/[...path]/+server.ts` — all four handlers are annotated `RequestHandler` while the
import of that type is commented out at line 4. import of that type is commented out at line 4. It also buffers whole responses with
- `src/app.d.ts``App.Locals["user"]` is typed from `lucide-svelte`'s `User` *icon* component `arrayBuffer()` and forwards no `Range` header, so book downloads are not streamed.
- `src/app.d.ts``App.Locals["user"]` is typed from `lucide-svelte`'s `User` _icon_ component
rather than the `User` interface in `$lib/server/auth`. rather than the `User` interface in `$lib/server/auth`.
- Uncommitted work in progress (as of 2026-08-10): library icons, spanning - No CSP, which foliate's README asks for because EPUBs can carry scripts. See `TODO.md` for why it
`components/ui/icon-picker/`, the newly vendored `components/ui/popover/`, is not enabled yet.
`forms/library-create-form.svelte`, `layout/library-switcher.svelte` and `schema/library.ts`.
Prefer not to refactor those files mid-flight.