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:
@@ -10,9 +10,11 @@ a KOSync-compatible endpoint.
|
||||
## Layout
|
||||
|
||||
| Path | What |
|
||||
| --- | --- |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `backend/` | Litestar REST API + PostgreSQL. See `backend/AGENTS.md`. |
|
||||
| `frontend/` | SvelteKit SSR web app. See `frontend/AGENTS.md`. |
|
||||
| `frontend/src/lib/vendor/` | Vendored `foliate-js` (the EPUB engine), copied by `frontend/scripts/vendor-foliate.sh`. |
|
||||
| `frontend/static/pdfjs/` | Vendored pdf.js viewer, used by the PDF reader in an iframe. |
|
||||
| `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. |
|
||||
|
||||
@@ -51,6 +51,29 @@ Worth adding at the same time:
|
||||
|
||||
## 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
|
||||
|
||||
`frontend/src/lib/reader/legacy-cache.ts` — `purgeLegacyLocationCache`
|
||||
|
||||
+74
-16
@@ -4,13 +4,16 @@ SvelteKit web app for the eBook library. See the repo-root `AGENTS.md` for the o
|
||||
dev-environment setup.
|
||||
|
||||
**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:
|
||||
`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
|
||||
live in `src/app.css`.
|
||||
Tailwind v4 has **no config file** — the theme, colour tokens (hex, not oklch) and
|
||||
`@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
|
||||
|
||||
@@ -65,8 +68,12 @@ Svelte context with a module-level `Symbol` key and a `setXState` / `getXState`
|
||||
|
||||
```ts
|
||||
const LIBRARY_KEY = Symbol('LIBRARY');
|
||||
export function setLibraryState(libraries: Library[]) { return setContext(LIBRARY_KEY, new LibraryState(libraries)); }
|
||||
export function getLibraryState() { return getContext<ReturnType<typeof setLibraryState>>(LIBRARY_KEY); }
|
||||
export function setLibraryState(libraries: Library[]) {
|
||||
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
|
||||
@@ -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
|
||||
rather than hand-writing them, and prefer wrapping over editing.
|
||||
- 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
|
||||
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
|
||||
|
||||
Route groups carry the layout structure:
|
||||
@@ -95,21 +148,26 @@ Route groups carry the layout structure:
|
||||
## Conventions
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
no `BookProgressRead`, so `book.progress` types as `{}`. This is the source of most of the ~104
|
||||
errors `pnpm check` reports on a clean tree; regenerating it should clear them. Get a baseline
|
||||
before assuming an error is yours.
|
||||
no `BookProgressRead`, so `book.progress` types as `{}`. It is the source of most of the errors
|
||||
`pnpm check` reports; regenerating it should clear them. **Baseline as of 2026-08-11: 37 errors,
|
||||
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
|
||||
import of that type is commented out at line 4.
|
||||
- `src/app.d.ts` — `App.Locals["user"]` is typed from `lucide-svelte`'s `User` *icon* component
|
||||
import of that type is commented out at line 4. It also buffers whole responses with
|
||||
`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`.
|
||||
- Uncommitted work in progress (as of 2026-08-10): library icons, spanning
|
||||
`components/ui/icon-picker/`, the newly vendored `components/ui/popover/`,
|
||||
`forms/library-create-form.svelte`, `layout/library-switcher.svelte` and `schema/library.ts`.
|
||||
Prefer not to refactor those files mid-flight.
|
||||
- No CSP, which foliate's README asks for because EPUBs can carry scripts. See `TODO.md` for why it
|
||||
is not enabled yet.
|
||||
|
||||
Reference in New Issue
Block a user