zreader 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +234 -2
- package/dist/blocks-DwR7l7pz.d.ts +20 -0
- package/dist/chunk-3WQBK6DE.js +47 -0
- package/dist/chunk-3WQBK6DE.js.map +1 -0
- package/dist/chunk-DYQNGV46.js +31 -0
- package/dist/chunk-DYQNGV46.js.map +1 -0
- package/dist/chunk-ISI35TCM.js +62 -0
- package/dist/chunk-ISI35TCM.js.map +1 -0
- package/dist/chunk-RNSAFDZ5.js +105 -0
- package/dist/chunk-RNSAFDZ5.js.map +1 -0
- package/dist/config/index.cjs +196 -0
- package/dist/config/index.cjs.map +1 -0
- package/dist/config/index.d.cts +159 -0
- package/dist/config/index.d.ts +159 -0
- package/dist/config/index.js +182 -0
- package/dist/config/index.js.map +1 -0
- package/dist/content/index.d.ts +143 -0
- package/dist/content/index.js +379 -0
- package/dist/content/index.js.map +1 -0
- package/dist/core/index.d.ts +262 -0
- package/dist/core/index.js +935 -0
- package/dist/core/index.js.map +1 -0
- package/dist/editor/index.d.ts +19 -0
- package/dist/editor/index.js +277 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/index-Bp5oIiZd.d.ts +61 -0
- package/dist/next/client/index.d.ts +8 -0
- package/dist/next/client/index.js +13 -0
- package/dist/next/client/index.js.map +1 -0
- package/dist/next/index.cjs +272 -0
- package/dist/next/index.cjs.map +1 -0
- package/dist/next/index.d.cts +47 -0
- package/dist/next/index.d.ts +47 -0
- package/dist/next/index.js +262 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/server/index.d.ts +58 -0
- package/dist/next/server/index.js +44 -0
- package/dist/next/server/index.js.map +1 -0
- package/dist/react/index.d.ts +376 -0
- package/dist/react/index.js +2793 -0
- package/dist/react/index.js.map +1 -0
- package/dist/reader.css +116 -0
- package/dist/rehype/index.d.ts +27 -0
- package/dist/rehype/index.js +4 -0
- package/dist/rehype/index.js.map +1 -0
- package/dist/stubs/empty-config.d.ts +9 -0
- package/dist/stubs/empty-config.js +7 -0
- package/dist/stubs/empty-config.js.map +1 -0
- package/dist/stubs/server-only.d.ts +2 -0
- package/dist/stubs/server-only.js +6 -0
- package/dist/stubs/server-only.js.map +1 -0
- package/dist/styles.css +1 -0
- package/dist/tailwind.css +41 -0
- package/dist/types-xkdpkPxA.d.ts +147 -0
- package/dist/user-config/index.d.ts +17 -0
- package/dist/user-config/index.js +35 -0
- package/dist/user-config/index.js.map +1 -0
- package/dist/virtual-config/index.d.ts +6 -0
- package/dist/virtual-config/index.js +10 -0
- package/dist/virtual-config/index.js.map +1 -0
- package/package.json +225 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- A React reader with a shelf, a contents page per book, and a chapter view.
|
|
8
|
+
- Bookmarks, reading progress and resume, sentence highlights in six colours with notes.
|
|
9
|
+
- A WYSIWYG Markdown scratchpad (Tiptap) per chapter, per book and globally, lazy-loaded from `zreader/editor`.
|
|
10
|
+
- Reading settings: theme (light, dark, sepia), text size, line spacing, measure and font.
|
|
11
|
+
- All reader data stays in `localStorage` under `zr:v1:*`, with JSON backup and restore.
|
|
12
|
+
- The `zreader/rehype` plugin adds block ids at build time; content without ids gets them in the browser.
|
|
13
|
+
- An optional content layer (`zreader/content`) for Markdown, MDX and HTML books.
|
|
14
|
+
- An optional Next.js adapter (`zreader/next`, `zreader/next/client`, `zreader/next/server`).
|
|
15
|
+
- Requires React 19 or later.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alchemyst AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,235 @@
|
|
|
1
|
-
#
|
|
1
|
+
# zreader
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A localStorage-first, mobile-first book reader for **React**. Readers get:
|
|
4
|
+
|
|
5
|
+
- **Bookmarks** and **reading progress**, which resumes where they left off.
|
|
6
|
+
- **Sentence highlights**: select text, or switch on highlight mode and tap a sentence. Highlights can carry **notes**.
|
|
7
|
+
- A **WYSIWYG Markdown scratchpad** (Tiptap) with checklists. There is a pad per chapter, per book and a global one, and you can quote highlights into it.
|
|
8
|
+
- Reading settings (theme, text size, line spacing and width, font) and JSON **backup and restore**.
|
|
9
|
+
|
|
10
|
+
All reader data stays in the browser. The UI is built from [shadcn/ui](https://ui.shadcn.com) components.
|
|
11
|
+
|
|
12
|
+
The core only needs React 19 or later. It works with Vite, React Router, TanStack, Remix or Next.js. Next.js gets an **optional adapter**.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
npm i zreader
|
|
16
|
+
# optional: the scratchpad editor
|
|
17
|
+
npm i @tiptap/react @tiptap/pm @tiptap/core @tiptap/starter-kit @tiptap/extension-list @tiptap/extensions @tiptap/markdown
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Entry points
|
|
21
|
+
|
|
22
|
+
| Import | Depends on | What it is |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| `zreader/react` | React | `ReaderProvider`, `BookShelf`, `BookContents`, `Reader`, hooks |
|
|
25
|
+
| `zreader/editor` | React, Tiptap | The scratchpad editor. The React layer lazy-loads it |
|
|
26
|
+
| `zreader` | — | Core: storage store, anchoring, block ids, backup |
|
|
27
|
+
| `zreader/rehype` | — | `rehypeBlocks` for any MDX/Markdown bundler, so block ids exist before the page loads |
|
|
28
|
+
| `zreader/content` | Node | Optional content layer for `content/books/**` (Markdown, MDX, HTML) |
|
|
29
|
+
| `zreader/config` | — | `defineConfig`, `glob`, `z` |
|
|
30
|
+
| `zreader/next` | Next.js | Optional adapter: `withReader()` for `next.config` |
|
|
31
|
+
| `zreader/next/client` | Next.js | Optional adapter: `NextReaderProvider` (provider with `next/link`) |
|
|
32
|
+
| `zreader/next/server` | Next.js | Optional adapter: `ReaderRoot`, `ReaderChapter`, `getBookParams`, `getChapterParams`, `mdxComponents` |
|
|
33
|
+
| `zreader/styles.css` | — | Precompiled stylesheet for apps without Tailwind |
|
|
34
|
+
| `zreader/tailwind.css` | — | For Tailwind v4 + shadcn apps; your own theme tokens apply |
|
|
35
|
+
|
|
36
|
+
A build check makes sure the `react`, `editor`, `rehype` and core bundles never import Next.js or Node built-ins.
|
|
37
|
+
|
|
38
|
+
## The provider
|
|
39
|
+
|
|
40
|
+
Everything renders under one `ReaderProvider`. It holds the reader's storage, theme and options, plus the **catalogue** of books. With the catalogue in place, the shelf, contents and reader components only need ids.
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { ReaderProvider } from "zreader/react";
|
|
44
|
+
import "zreader/styles.css";
|
|
45
|
+
|
|
46
|
+
const books = [
|
|
47
|
+
{
|
|
48
|
+
id: "guide",
|
|
49
|
+
title: "The Guide",
|
|
50
|
+
authors: ["A. Writer"],
|
|
51
|
+
chapters: [
|
|
52
|
+
{ id: "intro", title: "Introduction", order: 1 },
|
|
53
|
+
{ id: "setup", title: "Setup", order: 2, part: "Part I", label: "Chapter 2" },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
<ReaderProvider
|
|
59
|
+
books={books} // the catalogue (BookMeta[]): from JSON, a CMS, a fetch, /content…
|
|
60
|
+
Link={RouterLink} // any router's link; defaults to <a>
|
|
61
|
+
hrefFor={(book, chapter, hash) => …} // defaults to `${basePath}/${book}/${chapter}#hash`
|
|
62
|
+
headerSlot={<Logo />} // middle of the reader's top bar (optional)
|
|
63
|
+
options={{ basePath: "/read", highlightColors: ["yellow", "green"] }}
|
|
64
|
+
>
|
|
65
|
+
…
|
|
66
|
+
</ReaderProvider>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
| Component | Needs | Renders |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| `<BookShelf />` | the provider's `books` | Grid of book cards |
|
|
72
|
+
| `<BookContents book="guide" />` | a book id (or `BookMeta`) | A book's contents page: continue card and chapters grouped by `part` (alias: `BookHome`) |
|
|
73
|
+
| `<ContinueReading book="guide" />`, `<ChapterList book="guide" />` | a book id | The pieces of the contents page |
|
|
74
|
+
| `<Reader book="guide" chapter="intro">…</Reader>` | ids, plus the chapter as children (or `html`) | The full reader |
|
|
75
|
+
|
|
76
|
+
Props always override the provider: `Link`, `hrefFor`, `books`, `header`. `embedded` drops the full-page shell, so the shelf and contents page can sit inside your own layout. An unknown id throws an error that lists the ids it knows.
|
|
77
|
+
|
|
78
|
+
### The reader's top bar
|
|
79
|
+
|
|
80
|
+
The middle of the top bar takes any content. Set a default on the provider (`headerSlot`) or override it per chapter (`header`). It can be:
|
|
81
|
+
|
|
82
|
+
- a **string**, shown as text;
|
|
83
|
+
- an **element**, such as your logo or a description;
|
|
84
|
+
- a **function** of `{ book, chapter }`.
|
|
85
|
+
|
|
86
|
+
`<ReaderTitle />` is the default book-over-chapter title, so you can combine it with your own content:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
<ReaderProvider books={books} headerSlot={<><Logo /><ReaderTitle /></>}>
|
|
90
|
+
<Reader book="guide" chapter="intro" header={({ chapter }) => `Now reading: ${chapter.title}`}>…</Reader>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`useReaderChapter()` returns `{ book, chapter }` inside a `<Reader>`.
|
|
94
|
+
|
|
95
|
+
## Rendering MDX directly
|
|
96
|
+
|
|
97
|
+
Pass the chapter as children: an MDX component from your bundler, JSX, or anything else.
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import Intro from "./books/guide/intro.mdx";
|
|
101
|
+
|
|
102
|
+
<Reader book="guide" chapter="intro"><Intro /></Reader>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Highlights, notes, bookmarks and progress anchor to **block ids**. If the content has none, `<Reader>` adds them in the browser, using exactly the ids the build-time plugin would produce. With no `toc` prop, the chapter's contents list comes from its `h2`/`h3` headings.
|
|
106
|
+
|
|
107
|
+
To have the ids in the HTML from the first paint, add the plugin to your MDX pipeline:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// vite.config.ts
|
|
111
|
+
import mdx from "@mdx-js/rollup";
|
|
112
|
+
import { rehypeBlocks } from "zreader/rehype";
|
|
113
|
+
export default { plugins: [mdx({ rehypePlugins: [rehypeBlocks] })] };
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Plain React (Vite)
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import { BookShelf, BookContents, Reader, ReaderProvider } from "zreader/react";
|
|
120
|
+
import Intro from "./books/guide/intro.mdx";
|
|
121
|
+
|
|
122
|
+
export function App() {
|
|
123
|
+
return (
|
|
124
|
+
<ReaderProvider books={books} hrefFor={(b, c) => (c ? `#/read/${b}/${c}` : `#/read/${b}`)}>
|
|
125
|
+
<BookShelf />
|
|
126
|
+
<BookContents book="guide" />
|
|
127
|
+
<Reader book="guide" chapter="intro"><Intro /></Reader>
|
|
128
|
+
</ReaderProvider>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## React Router
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { Link, Route, Routes, useParams } from "react-router";
|
|
137
|
+
|
|
138
|
+
function BookPage() {
|
|
139
|
+
return <BookContents book={useParams().book!} />;
|
|
140
|
+
}
|
|
141
|
+
function ChapterPage() {
|
|
142
|
+
const { book, chapter } = useParams();
|
|
143
|
+
const Chapter = chapters[`${book}/${chapter}`]; // e.g. import.meta.glob("./books/**/*.mdx", { eager: true })
|
|
144
|
+
return <Reader book={book!} chapter={chapter!}><Chapter /></Reader>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
<ReaderProvider books={books} Link={({ href, ...p }) => <Link to={href} {...p} />} hrefFor={(b, c) => (c ? `/read/${b}/${c}` : `/read/${b}`)}>
|
|
148
|
+
<Routes>
|
|
149
|
+
<Route path="/read" element={<BookShelf />} />
|
|
150
|
+
<Route path="/read/:book" element={<BookPage />} />
|
|
151
|
+
<Route path="/read/:book/:chapter" element={<ChapterPage />} />
|
|
152
|
+
</Routes>
|
|
153
|
+
</ReaderProvider>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Next.js (optional adapter)
|
|
157
|
+
|
|
158
|
+
The adapter loads the catalogue on the server, binds `next/link`, and compiles chapters from `content/books/**`. In Next.js, `next/link` has to come from a `"use client"` module; `NextReaderProvider` is that module.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// next.config.ts
|
|
162
|
+
import { withReader } from "zreader/next";
|
|
163
|
+
export default withReader({ reactStrictMode: true });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
// app/books/layout.tsx — provider with the whole catalogue and next/link
|
|
168
|
+
import { ReaderRoot } from "zreader/next/server";
|
|
169
|
+
import "zreader/styles.css";
|
|
170
|
+
export default function Layout({ children }) {
|
|
171
|
+
return <ReaderRoot headerSlot={<YourLogo />}>{children}</ReaderRoot>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// app/books/page.tsx
|
|
175
|
+
import { BookShelf } from "zreader/react";
|
|
176
|
+
export default function Page() { return <BookShelf />; }
|
|
177
|
+
|
|
178
|
+
// app/books/[book]/page.tsx
|
|
179
|
+
import { BookContents } from "zreader/react";
|
|
180
|
+
import { getBookParams } from "zreader/next/server";
|
|
181
|
+
export const generateStaticParams = getBookParams;
|
|
182
|
+
export default async function Page({ params }) { return <BookContents book={(await params).book} />; }
|
|
183
|
+
|
|
184
|
+
// app/books/[book]/[chapter]/page.tsx — compiles Markdown/MDX/HTML and renders <Reader>
|
|
185
|
+
import { getChapterParams, ReaderChapter } from "zreader/next/server";
|
|
186
|
+
export const generateStaticParams = getChapterParams;
|
|
187
|
+
export default async function Page({ params }) { return <ReaderChapter {...await params} />; }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- `ReaderRoot` reads `config.reader` from `reader.config.ts` and loads every book. It takes the provider's props except `Link`; pass `books` to override the catalogue.
|
|
191
|
+
- `ReaderChapter` accepts extra MDX `components`, a per-chapter `header`, and any `<Reader>` prop.
|
|
192
|
+
- To use your own catalogue instead of the content layer, render `NextReaderProvider` from `next/client` yourself.
|
|
193
|
+
|
|
194
|
+
`withReader` does the following:
|
|
195
|
+
|
|
196
|
+
- Validates `reader.config.*` at startup.
|
|
197
|
+
- Aliases it for Turbopack and webpack.
|
|
198
|
+
- Keeps jsdom and DOMPurify external.
|
|
199
|
+
- Traces the content directory into the server output.
|
|
200
|
+
- Caches compiled chapters in `.next/cache/zreader`.
|
|
201
|
+
|
|
202
|
+
## Content layer (optional, Node)
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
content/books/<book-id>/
|
|
206
|
+
book.json { "title", "subtitle"?, "authors"?, "description"?, "chapters"?: [ids] }
|
|
207
|
+
01-introduction.md frontmatter: title, description?, part?, label?, number?, order?, draft?
|
|
208
|
+
02-deep-dive.mdx trusted MDX: compiled at build time, may use components
|
|
209
|
+
03-appendix.html raw HTML, sanitized with DOMPurify
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- `getBookMetas()` returns plain `BookMeta[]`, ready for `<ReaderProvider books>` in any framework.
|
|
213
|
+
- `getChapter()` and `renderChapter()` compile one chapter. `renderMarkdown()` and `renderHtml()` handle content from other sources.
|
|
214
|
+
|
|
215
|
+
**Security.** Markdown, inline HTML in Markdown, and `.html` chapters are all sanitized with DOMPurify. That removes scripts, event handlers, `javascript:` URLs, inline styles, forms, and iframes unless you allow them. **MDX is code**: compile only MDX files you trust and author yourself.
|
|
216
|
+
|
|
217
|
+
## Styling
|
|
218
|
+
|
|
219
|
+
- **Without Tailwind**, import `styles.css`. It is compiled and unlayered. Theme tokens are namespaced (`--zr-background`, `--zr-primary`, …, `--zr-font-serif`), so you can override them in your own CSS. A scoped reset keeps your site's global element styles out of the reader.
|
|
220
|
+
- **With Tailwind v4 + shadcn**, use `@import "zreader/tailwind.css";` instead. Your own tokens then style the reader.
|
|
221
|
+
- **Themes.** `next-themes`, a plain React library despite the name, sets `light`, `dark` or `sepia` on `<html>` and prevents a flash on load. If you already render a `ThemeProvider`, pass `manageTheme={false}`.
|
|
222
|
+
|
|
223
|
+
## Storage
|
|
224
|
+
|
|
225
|
+
Keys are namespaced as `zr:v1:*`, and each record is versioned with forward migrations. Corrupt records are dropped item by item, tabs stay in sync, writes are flushed when the page is hidden, and quota errors go to `onStorageError`. `exportBackup` and `importBackup` merge by id, and the most recently updated record wins. You can supply your own `StorageAdapter`, for example IndexedDB.
|
|
226
|
+
|
|
227
|
+
## Highlights
|
|
228
|
+
|
|
229
|
+
- **Anchors** use W3C-style quote and position selectors, so highlights survive edits to the content. When a passage moves, re-anchoring falls back in order: stored position, then the block's relative offset, then the quote with surrounding context, then a fuzzy match.
|
|
230
|
+
- **Painting** uses the CSS Custom Highlight API, so the DOM is never mutated.
|
|
231
|
+
- **Unplaced highlights** are kept and listed in the panel rather than deleted.
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT © Alchemyst AI
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
declare const BLOCK_TAG_NAMES: readonly ["p", "h1", "h2", "h3", "h4", "h5", "h6", "li", "blockquote", "pre", "figcaption", "td", "th", "dt", "dd"];
|
|
2
|
+
declare const BLOCK_TAGS: ReadonlySet<string>;
|
|
3
|
+
/** Collapse whitespace the same way on the server (hast) and in the DOM. */
|
|
4
|
+
declare function normalizeBlockText(text: string): string;
|
|
5
|
+
/** Hex digest of `text`, `length` characters long (up to 26). */
|
|
6
|
+
declare function hashString(text: string, length?: number): string;
|
|
7
|
+
/**
|
|
8
|
+
* Allocates content-derived block ids (`p-3fa9c1`) for one chapter. Inserting a
|
|
9
|
+
* paragraph doesn't shift other blocks' ids; repeated text gets `-2`, `-3`…
|
|
10
|
+
*/
|
|
11
|
+
declare function createBlockIdAllocator(): (tag: string, text: string) => string;
|
|
12
|
+
/** GitHub-style heading slugs, unique within one chapter. */
|
|
13
|
+
declare function createSlugger(): {
|
|
14
|
+
/** Slug for a heading's text. */
|
|
15
|
+
slug: (text: string) => string;
|
|
16
|
+
/** Keep an author-supplied id (made unique). */
|
|
17
|
+
keep: (id: string) => string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export { BLOCK_TAGS as B, BLOCK_TAG_NAMES as a, createSlugger as b, createBlockIdAllocator as c, hashString as h, normalizeBlockText as n };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createSlugger, createBlockIdAllocator, BLOCK_TAGS, normalizeBlockText, hashString } from './chunk-ISI35TCM.js';
|
|
2
|
+
import { toString } from 'hast-util-to-string';
|
|
3
|
+
import { visit } from 'unist-util-visit';
|
|
4
|
+
|
|
5
|
+
var HEADING = /^h([1-6])$/;
|
|
6
|
+
function containsBlock(node) {
|
|
7
|
+
for (const child of node.children) {
|
|
8
|
+
if (child.type === "element" && (BLOCK_TAGS.has(child.tagName) || containsBlock(child))) return true;
|
|
9
|
+
}
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
function rehypeBlocks(options = {}) {
|
|
13
|
+
const [minDepth, maxDepth] = options.tocDepth ?? [2, 3];
|
|
14
|
+
return (tree, file) => {
|
|
15
|
+
const slugger = createSlugger();
|
|
16
|
+
const allocate = createBlockIdAllocator();
|
|
17
|
+
const toc = [];
|
|
18
|
+
const blocks = [];
|
|
19
|
+
const texts = [];
|
|
20
|
+
visit(tree, "element", (node) => {
|
|
21
|
+
if (!BLOCK_TAGS.has(node.tagName)) return;
|
|
22
|
+
const text = normalizeBlockText(toString(node));
|
|
23
|
+
const heading = HEADING.exec(node.tagName);
|
|
24
|
+
node.properties ??= {};
|
|
25
|
+
if (heading) {
|
|
26
|
+
const existing = typeof node.properties.id === "string" ? node.properties.id : "";
|
|
27
|
+
const id2 = existing ? slugger.keep(existing) : slugger.slug(text);
|
|
28
|
+
node.properties.id = id2;
|
|
29
|
+
const depth = Number(heading[1]);
|
|
30
|
+
if (depth >= minDepth && depth <= maxDepth && text) toc.push({ id: id2, depth, text });
|
|
31
|
+
}
|
|
32
|
+
if (containsBlock(node)) return;
|
|
33
|
+
if (!text) return;
|
|
34
|
+
const id = allocate(node.tagName, text);
|
|
35
|
+
node.properties.dataBlockId = id;
|
|
36
|
+
blocks.push({ id, tag: node.tagName, excerpt: text.slice(0, 80) });
|
|
37
|
+
texts.push(text);
|
|
38
|
+
});
|
|
39
|
+
const data = { toc, blocks, contentHash: hashString(texts.join("\n")) };
|
|
40
|
+
file.data.reader = data;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
var rehype_default = rehypeBlocks;
|
|
44
|
+
|
|
45
|
+
export { rehypeBlocks, rehype_default };
|
|
46
|
+
//# sourceMappingURL=chunk-3WQBK6DE.js.map
|
|
47
|
+
//# sourceMappingURL=chunk-3WQBK6DE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rehype/index.ts"],"names":["id"],"mappings":";;;;AAiBA,IAAM,OAAA,GAAU,YAAA;AAehB,SAAS,cAAc,IAAA,EAAwB;AAC7C,EAAA,KAAA,MAAW,KAAA,IAAS,KAAK,QAAA,EAAU;AACjC,IAAA,IAAI,KAAA,CAAM,IAAA,KAAS,SAAA,KAAc,UAAA,CAAW,GAAA,CAAI,KAAA,CAAM,OAAO,CAAA,IAAK,aAAA,CAAc,KAAK,CAAA,CAAA,EAAI,OAAO,IAAA;AAAA,EAClG;AACA,EAAA,OAAO,KAAA;AACT;AAWO,SAAS,YAAA,CAAa,OAAA,GAA+B,EAAC,EAAG;AAC9D,EAAA,MAAM,CAAC,UAAU,QAAQ,CAAA,GAAI,QAAQ,QAAA,IAAY,CAAC,GAAG,CAAC,CAAA;AACtD,EAAA,OAAO,CAAC,MAAY,IAAA,KAAgB;AAClC,IAAA,MAAM,UAAU,aAAA,EAAc;AAC9B,IAAA,MAAM,WAAW,sBAAA,EAAuB;AACxC,IAAA,MAAM,MAAkB,EAAC;AACzB,IAAA,MAAM,SAAsB,EAAC;AAC7B,IAAA,MAAM,QAAkB,EAAC;AAEzB,IAAA,KAAA,CAAM,IAAA,EAAM,SAAA,EAAW,CAAC,IAAA,KAAkB;AACxC,MAAA,IAAI,CAAC,UAAA,CAAW,GAAA,CAAI,IAAA,CAAK,OAAO,CAAA,EAAG;AACnC,MAAA,MAAM,IAAA,GAAO,kBAAA,CAAmB,QAAA,CAAS,IAAI,CAAC,CAAA;AAC9C,MAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,IAAA,CAAK,IAAA,CAAK,OAAO,CAAA;AACzC,MAAA,IAAA,CAAK,eAAe,EAAC;AAErB,MAAA,IAAI,OAAA,EAAS;AACX,QAAA,MAAM,QAAA,GAAW,OAAO,IAAA,CAAK,UAAA,CAAW,OAAO,QAAA,GAAW,IAAA,CAAK,WAAW,EAAA,GAAK,EAAA;AAC/E,QAAA,MAAMA,GAAAA,GAAK,WAAW,OAAA,CAAQ,IAAA,CAAK,QAAQ,CAAA,GAAI,OAAA,CAAQ,KAAK,IAAI,CAAA;AAChE,QAAA,IAAA,CAAK,WAAW,EAAA,GAAKA,GAAAA;AACrB,QAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,OAAA,CAAQ,CAAC,CAAC,CAAA;AAC/B,QAAA,IAAI,KAAA,IAAS,QAAA,IAAY,KAAA,IAAS,QAAA,IAAY,IAAA,EAAM,GAAA,CAAI,IAAA,CAAK,EAAE,EAAA,EAAAA,GAAAA,EAAI,KAAA,EAAO,IAAA,EAAM,CAAA;AAAA,MAClF;AAEA,MAAA,IAAI,aAAA,CAAc,IAAI,CAAA,EAAG;AACzB,MAAA,IAAI,CAAC,IAAA,EAAM;AACX,MAAA,MAAM,EAAA,GAAK,QAAA,CAAS,IAAA,CAAK,OAAA,EAAS,IAAI,CAAA;AACtC,MAAA,IAAA,CAAK,WAAW,WAAA,GAAc,EAAA;AAC9B,MAAA,MAAA,CAAO,IAAA,CAAK,EAAE,EAAA,EAAI,GAAA,EAAK,IAAA,CAAK,OAAA,EAAS,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,EAAG,CAAA;AACjE,MAAA,KAAA,CAAM,KAAK,IAAI,CAAA;AAAA,IACjB,CAAC,CAAA;AAED,IAAA,MAAM,IAAA,GAAmB,EAAE,GAAA,EAAK,MAAA,EAAQ,WAAA,EAAa,WAAW,KAAA,CAAM,IAAA,CAAK,IAAI,CAAC,CAAA,EAAE;AAClF,IAAC,IAAA,CAAK,KAAiC,MAAA,GAAS,IAAA;AAAA,EAClD,CAAA;AACF;AAEA,IAAO,cAAA,GAAQ","file":"chunk-3WQBK6DE.js","sourcesContent":["/**\n * `zreader/rehype` — the block-id plugin for any MDX/Markdown\n * pipeline (Vite's @mdx-js/rollup, @next/mdx, unified…). Runs anywhere: no Node APIs.\n *\n * // vite.config.ts\n * mdx({ rehypePlugins: [rehypeBlocks] })\n *\n * Without it, <Reader> assigns the same ids in the browser; with it, they are in\n * the HTML from the start.\n */\nimport { toString } from \"hast-util-to-string\";\nimport type { Element, Root } from \"hast\";\nimport { visit } from \"unist-util-visit\";\nimport type { VFile } from \"vfile\";\nimport { BLOCK_TAGS, createBlockIdAllocator, createSlugger, hashString, normalizeBlockText } from \"../core/blocks\";\nimport type { TocEntry } from \"../core/types\";\n\nconst HEADING = /^h([1-6])$/;\n\nexport interface BlockInfo {\n id: string;\n tag: string;\n /** First 80 chars, for bookmarks/progress UIs. */\n excerpt: string;\n}\n\nexport interface BlocksData {\n toc: TocEntry[];\n blocks: BlockInfo[];\n contentHash: string;\n}\n\nfunction containsBlock(node: Element): boolean {\n for (const child of node.children) {\n if (child.type === \"element\" && (BLOCK_TAGS.has(child.tagName) || containsBlock(child))) return true;\n }\n return false;\n}\n\nexport interface RehypeBlocksOptions {\n /** Heading depths included in the TOC. Default: 2–3 (h1 is the chapter title). */\n tocDepth?: [min: number, max: number];\n}\n\n/**\n * Adds stable `data-block-id` attributes to leaf blocks, slug `id`s to headings,\n * and collects `{ toc, blocks, contentHash }` into `file.data.reader`.\n */\nexport function rehypeBlocks(options: RehypeBlocksOptions = {}) {\n const [minDepth, maxDepth] = options.tocDepth ?? [2, 3];\n return (tree: Root, file: VFile) => {\n const slugger = createSlugger();\n const allocate = createBlockIdAllocator();\n const toc: TocEntry[] = [];\n const blocks: BlockInfo[] = [];\n const texts: string[] = [];\n\n visit(tree, \"element\", (node: Element) => {\n if (!BLOCK_TAGS.has(node.tagName)) return;\n const text = normalizeBlockText(toString(node));\n const heading = HEADING.exec(node.tagName);\n node.properties ??= {};\n\n if (heading) {\n const existing = typeof node.properties.id === \"string\" ? node.properties.id : \"\";\n const id = existing ? slugger.keep(existing) : slugger.slug(text);\n node.properties.id = id;\n const depth = Number(heading[1]);\n if (depth >= minDepth && depth <= maxDepth && text) toc.push({ id, depth, text });\n }\n\n if (containsBlock(node)) return; // only innermost blocks are anchors\n if (!text) return;\n const id = allocate(node.tagName, text);\n node.properties.dataBlockId = id;\n blocks.push({ id, tag: node.tagName, excerpt: text.slice(0, 80) });\n texts.push(text);\n });\n\n const data: BlocksData = { toc, blocks, contentHash: hashString(texts.join(\"\\n\")) };\n (file.data as { reader?: BlocksData }).reader = data;\n };\n}\n\nexport default rehypeBlocks;\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/core/types.ts
|
|
2
|
+
var THEMES = ["system", "light", "sepia", "dark"];
|
|
3
|
+
var TEXT_SIZES = ["sm", "base", "lg", "xl", "2xl"];
|
|
4
|
+
var LEADINGS = ["tight", "normal", "relaxed"];
|
|
5
|
+
var MEASURES = ["narrow", "normal", "wide"];
|
|
6
|
+
var FONT_FAMILIES = ["serif", "sans", "mono"];
|
|
7
|
+
var HIGHLIGHT_COLORS = ["yellow", "green", "blue", "pink", "purple", "orange"];
|
|
8
|
+
|
|
9
|
+
// src/core/defaults.ts
|
|
10
|
+
var DEFAULT_CLIENT_OPTIONS = {
|
|
11
|
+
storagePrefix: "zr",
|
|
12
|
+
highlightColors: ["yellow", "green", "blue", "pink"],
|
|
13
|
+
defaultHighlightColor: "yellow",
|
|
14
|
+
scratchpad: { enabled: true, scopes: ["chapter", "book", "global"] },
|
|
15
|
+
themes: ["system", "light", "sepia", "dark"],
|
|
16
|
+
defaultTheme: "system",
|
|
17
|
+
defaultSettings: {
|
|
18
|
+
textSize: "lg",
|
|
19
|
+
leading: "relaxed",
|
|
20
|
+
measure: "normal",
|
|
21
|
+
fontFamily: "serif",
|
|
22
|
+
highlightMode: false
|
|
23
|
+
},
|
|
24
|
+
progress: { completeAt: 0.9, saveIntervalMs: 1e3 },
|
|
25
|
+
backupReminder: true,
|
|
26
|
+
basePath: "/books"
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export { DEFAULT_CLIENT_OPTIONS, FONT_FAMILIES, HIGHLIGHT_COLORS, LEADINGS, MEASURES, TEXT_SIZES, THEMES };
|
|
30
|
+
//# sourceMappingURL=chunk-DYQNGV46.js.map
|
|
31
|
+
//# sourceMappingURL=chunk-DYQNGV46.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/types.ts","../src/core/defaults.ts"],"names":[],"mappings":";AAgFO,IAAM,MAAA,GAAS,CAAC,QAAA,EAAU,OAAA,EAAS,SAAS,MAAM;AAClD,IAAM,aAAa,CAAC,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,MAAM,KAAK;AACnD,IAAM,QAAA,GAAW,CAAC,OAAA,EAAS,QAAA,EAAU,SAAS;AAC9C,IAAM,QAAA,GAAW,CAAC,QAAA,EAAU,QAAA,EAAU,MAAM;AAC5C,IAAM,aAAA,GAAgB,CAAC,OAAA,EAAS,MAAA,EAAQ,MAAM;AAE9C,IAAM,mBAAmB,CAAC,QAAA,EAAU,SAAS,MAAA,EAAQ,MAAA,EAAQ,UAAU,QAAQ;;;ACpF/E,IAAM,sBAAA,GAA8C;AAAA,EACzD,aAAA,EAAe,IAAA;AAAA,EACf,eAAA,EAAiB,CAAC,QAAA,EAAU,OAAA,EAAS,QAAQ,MAAM,CAAA;AAAA,EACnD,qBAAA,EAAuB,QAAA;AAAA,EACvB,UAAA,EAAY,EAAE,OAAA,EAAS,IAAA,EAAM,QAAQ,CAAC,SAAA,EAAW,MAAA,EAAQ,QAAQ,CAAA,EAAE;AAAA,EACnE,MAAA,EAAQ,CAAC,QAAA,EAAU,OAAA,EAAS,SAAS,MAAM,CAAA;AAAA,EAC3C,YAAA,EAAc,QAAA;AAAA,EACd,eAAA,EAAiB;AAAA,IACf,QAAA,EAAU,IAAA;AAAA,IACV,OAAA,EAAS,SAAA;AAAA,IACT,OAAA,EAAS,QAAA;AAAA,IACT,UAAA,EAAY,OAAA;AAAA,IACZ,aAAA,EAAe;AAAA,GACjB;AAAA,EACA,QAAA,EAAU,EAAE,UAAA,EAAY,GAAA,EAAK,gBAAgB,GAAA,EAAK;AAAA,EAClD,cAAA,EAAgB,IAAA;AAAA,EAChB,QAAA,EAAU;AACZ","file":"chunk-DYQNGV46.js","sourcesContent":["/** Shared, framework-agnostic data model for zreader. */\n\nexport type ISODate = string;\n\n/** W3C Web Annotation-style anchor. Offsets are measured over the chapter root's text. */\nexport interface Anchor {\n /** `data-block-id` of the block containing the start of the selection. */\n blockId: string;\n /** Offsets relative to the start block's text (fast path). */\n blockStart: number;\n /** TextPositionSelector over the whole chapter text. */\n start: number;\n end: number;\n /** TextQuoteSelector. */\n exact: string;\n prefix: string;\n suffix: string;\n /** Content hash of the chapter when the anchor was created. */\n contentHash?: string;\n}\n\nexport interface Highlight {\n id: string;\n bookId: string;\n chapterId: string;\n anchor: Anchor;\n color: string;\n note?: string;\n createdAt: ISODate;\n updatedAt: ISODate;\n}\n\nexport interface Bookmark {\n id: string;\n bookId: string;\n chapterId: string;\n /** Absent for whole-chapter bookmarks. */\n blockId?: string;\n label?: string;\n excerpt?: string;\n createdAt: ISODate;\n updatedAt: ISODate;\n}\n\nexport interface ChapterProgress {\n chapterId: string;\n blockId?: string;\n /** 0..1 */\n percent: number;\n completed: boolean;\n updatedAt: ISODate;\n}\n\nexport interface BookProgress {\n bookId: string;\n chapters: Record<string, ChapterProgress>;\n last?: { chapterId: string; blockId?: string; updatedAt: ISODate };\n}\n\nexport type PadScope =\n | { kind: \"global\" }\n | { kind: \"book\"; bookId: string }\n | { kind: \"chapter\"; bookId: string; chapterId: string };\n\nexport type PadScopeKind = PadScope[\"kind\"];\n\nexport interface Pad {\n /** Markdown is the interchange/source format. */\n markdown: string;\n /** Lossless editor JSON cache. Cleared whenever `markdown` is changed outside the editor. */\n json?: unknown;\n updatedAt: ISODate;\n}\n\nexport type ThemeName = \"system\" | \"light\" | \"sepia\" | \"dark\";\nexport type Measure = \"narrow\" | \"normal\" | \"wide\";\nexport type FontFamily = \"serif\" | \"sans\" | \"mono\";\nexport type TextSize = \"sm\" | \"base\" | \"lg\" | \"xl\" | \"2xl\";\nexport type Leading = \"tight\" | \"normal\" | \"relaxed\";\n\nexport const THEMES = [\"system\", \"light\", \"sepia\", \"dark\"] as const satisfies readonly ThemeName[];\nexport const TEXT_SIZES = [\"sm\", \"base\", \"lg\", \"xl\", \"2xl\"] as const satisfies readonly TextSize[];\nexport const LEADINGS = [\"tight\", \"normal\", \"relaxed\"] as const satisfies readonly Leading[];\nexport const MEASURES = [\"narrow\", \"normal\", \"wide\"] as const satisfies readonly Measure[];\nexport const FONT_FAMILIES = [\"serif\", \"sans\", \"mono\"] as const satisfies readonly FontFamily[];\n/** The palette the UI ships styles for (`::highlight(zr-<color>)`). */\nexport const HIGHLIGHT_COLORS = [\"yellow\", \"green\", \"blue\", \"pink\", \"purple\", \"orange\"] as const;\nexport type HighlightColor = (typeof HIGHLIGHT_COLORS)[number];\n\nexport interface ReaderSettings {\n textSize: TextSize;\n leading: Leading;\n measure: Measure;\n fontFamily: FontFamily;\n /** Tap a sentence to highlight it with the default colour. */\n highlightMode: boolean;\n updatedAt: ISODate;\n}\n\n/** Serializable client options (the `reader` half of reader.config.ts). */\nexport interface ReaderClientOptions {\n storagePrefix: string;\n highlightColors: HighlightColor[];\n defaultHighlightColor: HighlightColor;\n scratchpad: { enabled: boolean; scopes: PadScopeKind[] };\n /** Themes offered in the UI. Applied as a class on <html> via next-themes. */\n themes: ThemeName[];\n defaultTheme: ThemeName;\n defaultSettings: Omit<ReaderSettings, \"updatedAt\">;\n progress: { completeAt: number; saveIntervalMs: number };\n /** Show a \"your notes only live in this browser\" reminder. */\n backupReminder: boolean;\n /** Base path used to build links back into the reader (e.g. quote-to-pad). */\n basePath: string;\n}\n\nexport interface TocEntry {\n id: string;\n depth: number;\n text: string;\n}\n\nexport interface ChapterMeta {\n id: string;\n title: string;\n order: number;\n description?: string;\n /** Section the chapter belongs to, e.g. \"Part I · For the Builders\". Groups the contents list. */\n part?: string;\n /** Eyebrow shown above the title, e.g. \"Chapter 5\" or \"Appendix A\". */\n label?: string;\n /** Short marker for lists, e.g. \"5\" or \"A\". */\n number?: string;\n}\n\nexport interface BookMeta {\n id: string;\n title: string;\n subtitle?: string;\n authors: string[];\n description?: string;\n cover?: string;\n chapters: ChapterMeta[];\n}\n","import type { ReaderClientOptions } from \"./types\";\n\nexport const DEFAULT_CLIENT_OPTIONS: ReaderClientOptions = {\n storagePrefix: \"zr\",\n highlightColors: [\"yellow\", \"green\", \"blue\", \"pink\"],\n defaultHighlightColor: \"yellow\",\n scratchpad: { enabled: true, scopes: [\"chapter\", \"book\", \"global\"] },\n themes: [\"system\", \"light\", \"sepia\", \"dark\"],\n defaultTheme: \"system\",\n defaultSettings: {\n textSize: \"lg\",\n leading: \"relaxed\",\n measure: \"normal\",\n fontFamily: \"serif\",\n highlightMode: false,\n },\n progress: { completeAt: 0.9, saveIntervalMs: 1000 },\n backupReminder: true,\n basePath: \"/books\",\n};\n"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import GithubSlugger from 'github-slugger';
|
|
2
|
+
|
|
3
|
+
// src/core/blocks.ts
|
|
4
|
+
var BLOCK_TAG_NAMES = [
|
|
5
|
+
"p",
|
|
6
|
+
"h1",
|
|
7
|
+
"h2",
|
|
8
|
+
"h3",
|
|
9
|
+
"h4",
|
|
10
|
+
"h5",
|
|
11
|
+
"h6",
|
|
12
|
+
"li",
|
|
13
|
+
"blockquote",
|
|
14
|
+
"pre",
|
|
15
|
+
"figcaption",
|
|
16
|
+
"td",
|
|
17
|
+
"th",
|
|
18
|
+
"dt",
|
|
19
|
+
"dd"
|
|
20
|
+
];
|
|
21
|
+
var BLOCK_TAGS = new Set(BLOCK_TAG_NAMES);
|
|
22
|
+
function normalizeBlockText(text) {
|
|
23
|
+
return text.replace(/\s+/g, " ").trim();
|
|
24
|
+
}
|
|
25
|
+
function cyrb53(str, seed = 0) {
|
|
26
|
+
let h1 = 3735928559 ^ seed;
|
|
27
|
+
let h2 = 1103547991 ^ seed;
|
|
28
|
+
for (let i = 0; i < str.length; i++) {
|
|
29
|
+
const ch = str.charCodeAt(i);
|
|
30
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
31
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
32
|
+
}
|
|
33
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
34
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
35
|
+
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
36
|
+
}
|
|
37
|
+
function hashString(text, length = 16) {
|
|
38
|
+
const hex = cyrb53(text, 0).toString(16).padStart(14, "0") + cyrb53(text, 2654435769).toString(16).padStart(14, "0");
|
|
39
|
+
return hex.slice(0, length);
|
|
40
|
+
}
|
|
41
|
+
function createBlockIdAllocator() {
|
|
42
|
+
const seen = /* @__PURE__ */ new Map();
|
|
43
|
+
return (tag, text) => {
|
|
44
|
+
const base = `${tag.toLowerCase()}-${hashString(normalizeBlockText(text), 6)}`;
|
|
45
|
+
const n = (seen.get(base) ?? 0) + 1;
|
|
46
|
+
seen.set(base, n);
|
|
47
|
+
return n === 1 ? base : `${base}-${n}`;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function createSlugger() {
|
|
51
|
+
const slugger = new GithubSlugger();
|
|
52
|
+
return {
|
|
53
|
+
/** Slug for a heading's text. */
|
|
54
|
+
slug: (text) => slugger.slug(text || "section"),
|
|
55
|
+
/** Keep an author-supplied id (made unique). */
|
|
56
|
+
keep: (id) => slugger.slug(id, true)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { BLOCK_TAGS, BLOCK_TAG_NAMES, createBlockIdAllocator, createSlugger, hashString, normalizeBlockText };
|
|
61
|
+
//# sourceMappingURL=chunk-ISI35TCM.js.map
|
|
62
|
+
//# sourceMappingURL=chunk-ISI35TCM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/blocks.ts"],"names":[],"mappings":"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","file":"chunk-ISI35TCM.js","sourcesContent":["/**\n * Block ids and heading slugs, shared by the build-time rehype plugin\n * (`zreader/rehype`) and the in-browser fallback in <Reader>, so\n * both produce identical ids for the same content. No Node APIs.\n */\nimport GithubSlugger from \"github-slugger\";\n\nexport const BLOCK_TAG_NAMES = [\n \"p\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"li\",\n \"blockquote\",\n \"pre\",\n \"figcaption\",\n \"td\",\n \"th\",\n \"dt\",\n \"dd\",\n] as const;\n\nexport const BLOCK_TAGS: ReadonlySet<string> = new Set(BLOCK_TAG_NAMES);\n\n/** Collapse whitespace the same way on the server (hast) and in the DOM. */\nexport function normalizeBlockText(text: string): string {\n return text.replace(/\\s+/g, \" \").trim();\n}\n\n/** cyrb53: a fast, well-distributed 53-bit string hash (public domain). */\nfunction cyrb53(str: string, seed = 0): number {\n let h1 = 0xdeadbeef ^ seed;\n let h2 = 0x41c6ce57 ^ seed;\n for (let i = 0; i < str.length; i++) {\n const ch = str.charCodeAt(i);\n h1 = Math.imul(h1 ^ ch, 2654435761);\n h2 = Math.imul(h2 ^ ch, 1597334677);\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n return 4294967296 * (2097151 & h2) + (h1 >>> 0);\n}\n\n/** Hex digest of `text`, `length` characters long (up to 26). */\nexport function hashString(text: string, length = 16): string {\n const hex = cyrb53(text, 0).toString(16).padStart(14, \"0\") + cyrb53(text, 0x9e3779b9).toString(16).padStart(14, \"0\");\n return hex.slice(0, length);\n}\n\n/**\n * Allocates content-derived block ids (`p-3fa9c1`) for one chapter. Inserting a\n * paragraph doesn't shift other blocks' ids; repeated text gets `-2`, `-3`…\n */\nexport function createBlockIdAllocator() {\n const seen = new Map<string, number>();\n return (tag: string, text: string): string => {\n const base = `${tag.toLowerCase()}-${hashString(normalizeBlockText(text), 6)}`;\n const n = (seen.get(base) ?? 0) + 1;\n seen.set(base, n);\n return n === 1 ? base : `${base}-${n}`;\n };\n}\n\n/** GitHub-style heading slugs, unique within one chapter. */\nexport function createSlugger() {\n const slugger = new GithubSlugger();\n return {\n /** Slug for a heading's text. */\n slug: (text: string) => slugger.slug(text || \"section\"),\n /** Keep an author-supplied id (made unique). */\n keep: (id: string) => slugger.slug(id, true),\n };\n}\n"]}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { THEMES, HIGHLIGHT_COLORS, FONT_FAMILIES, MEASURES, LEADINGS, TEXT_SIZES } from './chunk-DYQNGV46.js';
|
|
2
|
+
import { z } from 'zod/v4';
|
|
3
|
+
export { z } from 'zod/v4';
|
|
4
|
+
|
|
5
|
+
function glob(opts) {
|
|
6
|
+
return { kind: "glob", base: opts.base, pattern: opts.pattern ?? "**/*.{md,mdx,html}", ignore: opts.ignore };
|
|
7
|
+
}
|
|
8
|
+
z.looseObject({
|
|
9
|
+
title: z.string(),
|
|
10
|
+
subtitle: z.string().optional(),
|
|
11
|
+
authors: z.array(z.string()).default([]),
|
|
12
|
+
description: z.string().optional(),
|
|
13
|
+
cover: z.string().optional(),
|
|
14
|
+
/** Optional explicit chapter order (chapter slugs). */
|
|
15
|
+
chapters: z.array(z.string()).optional()
|
|
16
|
+
});
|
|
17
|
+
z.looseObject({
|
|
18
|
+
title: z.string(),
|
|
19
|
+
order: z.number().optional(),
|
|
20
|
+
description: z.string().optional(),
|
|
21
|
+
part: z.string().optional(),
|
|
22
|
+
label: z.string().optional(),
|
|
23
|
+
number: z.union([z.string(), z.number()]).transform(String).optional(),
|
|
24
|
+
draft: z.boolean().default(false)
|
|
25
|
+
});
|
|
26
|
+
function defineConfig(config) {
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
29
|
+
function isPlainObject(x) {
|
|
30
|
+
if (typeof x !== "object" || x === null || Array.isArray(x)) return false;
|
|
31
|
+
const proto = Object.getPrototypeOf(x);
|
|
32
|
+
return proto === Object.prototype || proto === null;
|
|
33
|
+
}
|
|
34
|
+
function mergeConfig(base, ...overrides) {
|
|
35
|
+
let out = base;
|
|
36
|
+
for (const o of overrides) out = mergeTwo(out, o);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
function mergeTwo(a, b) {
|
|
40
|
+
if (b === void 0) return a;
|
|
41
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
42
|
+
const out = { ...a };
|
|
43
|
+
for (const [k, v] of Object.entries(b)) out[k] = mergeTwo(a[k], v);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
return b;
|
|
47
|
+
}
|
|
48
|
+
var themeName = z.enum(THEMES);
|
|
49
|
+
var highlightColor = z.enum(HIGHLIGHT_COLORS);
|
|
50
|
+
z.object({
|
|
51
|
+
storagePrefix: z.string().regex(/^[a-z0-9_-]+$/i, "use letters, digits, '-' or '_'"),
|
|
52
|
+
highlightColors: z.array(highlightColor).min(1, "must be non-empty"),
|
|
53
|
+
defaultHighlightColor: highlightColor,
|
|
54
|
+
scratchpad: z.object({
|
|
55
|
+
enabled: z.boolean(),
|
|
56
|
+
scopes: z.array(z.enum(["global", "book", "chapter"])).min(1, "must be non-empty")
|
|
57
|
+
}),
|
|
58
|
+
themes: z.array(themeName).min(1),
|
|
59
|
+
defaultTheme: themeName,
|
|
60
|
+
defaultSettings: z.object({
|
|
61
|
+
textSize: z.enum(TEXT_SIZES),
|
|
62
|
+
leading: z.enum(LEADINGS),
|
|
63
|
+
measure: z.enum(MEASURES),
|
|
64
|
+
fontFamily: z.enum(FONT_FAMILIES),
|
|
65
|
+
highlightMode: z.boolean()
|
|
66
|
+
}),
|
|
67
|
+
progress: z.object({ completeAt: z.number().gt(0).max(1), saveIntervalMs: z.number().min(100) }),
|
|
68
|
+
backupReminder: z.boolean(),
|
|
69
|
+
basePath: z.string().startsWith("/")
|
|
70
|
+
}).refine((c) => c.highlightColors.includes(c.defaultHighlightColor), {
|
|
71
|
+
message: "defaultHighlightColor must be one of highlightColors",
|
|
72
|
+
path: ["defaultHighlightColor"]
|
|
73
|
+
}).refine((c) => c.themes.includes(c.defaultTheme), {
|
|
74
|
+
message: "defaultTheme must be one of themes",
|
|
75
|
+
path: ["defaultTheme"]
|
|
76
|
+
});
|
|
77
|
+
function isSchemaLike(v) {
|
|
78
|
+
return typeof v === "object" && v !== null && typeof v.safeParse === "function";
|
|
79
|
+
}
|
|
80
|
+
z.object({
|
|
81
|
+
loader: z.object({
|
|
82
|
+
kind: z.literal("glob"),
|
|
83
|
+
base: z.string().min(1),
|
|
84
|
+
pattern: z.union([z.string(), z.array(z.string()).min(1)]),
|
|
85
|
+
ignore: z.array(z.string()).optional()
|
|
86
|
+
}),
|
|
87
|
+
schemas: z.object({
|
|
88
|
+
book: z.custom(isSchemaLike, "must be a zod (or safeParse-compatible) schema"),
|
|
89
|
+
chapter: z.custom(isSchemaLike, "must be a zod (or safeParse-compatible) schema")
|
|
90
|
+
}),
|
|
91
|
+
remarkPlugins: z.array(z.any()),
|
|
92
|
+
rehypePlugins: z.array(z.any()),
|
|
93
|
+
sanitize: z.object({
|
|
94
|
+
allowIframes: z.array(z.string()).optional(),
|
|
95
|
+
allowTags: z.array(z.string()).optional(),
|
|
96
|
+
allowAttributes: z.array(z.string()).optional()
|
|
97
|
+
}),
|
|
98
|
+
includeDrafts: z.boolean(),
|
|
99
|
+
cacheDir: z.string()
|
|
100
|
+
});
|
|
101
|
+
var CONFIG_FILE_NAMES = ["reader.config.ts", "reader.config.mts", "reader.config.mjs", "reader.config.js"];
|
|
102
|
+
|
|
103
|
+
export { CONFIG_FILE_NAMES, defineConfig, glob, mergeConfig };
|
|
104
|
+
//# sourceMappingURL=chunk-RNSAFDZ5.js.map
|
|
105
|
+
//# sourceMappingURL=chunk-RNSAFDZ5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config/index.ts"],"names":[],"mappings":"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","file":"chunk-RNSAFDZ5.js","sourcesContent":["import { z } from \"zod/v4\";\nimport { DEFAULT_CLIENT_OPTIONS } from \"../core/defaults\";\nimport {\n FONT_FAMILIES,\n HIGHLIGHT_COLORS,\n LEADINGS,\n MEASURES,\n TEXT_SIZES,\n THEMES,\n type ReaderClientOptions,\n} from \"../core/types\";\n\nexport { z };\n\n// ---------------------------------------------------------------- types\n\nexport interface GlobLoader {\n kind: \"glob\";\n /** Directory containing one folder per book. Relative to the project root. */\n base: string;\n /** Chapter files, relative to `base`. */\n pattern: string | string[];\n ignore?: string[];\n}\n\n/** Astro-style glob loader: `content/books/<book>/{book.json, 01-intro.md, ...}` */\nexport function glob(opts: { base: string; pattern?: string | string[]; ignore?: string[] }): GlobLoader {\n return { kind: \"glob\", base: opts.base, pattern: opts.pattern ?? \"**/*.{md,mdx,html}\", ignore: opts.ignore };\n}\n\n// unified's PluggableList, kept loose to avoid a type dependency in this entry.\nexport type PluginList = unknown[];\n\nexport interface SanitizeOptions {\n /** Hostnames allowed for <iframe src>. Iframes are stripped unless listed here. */\n allowIframes?: string[];\n /** Extra tags to allow on top of DOMPurify's safe HTML profile. */\n allowTags?: string[];\n /** Extra attributes to allow. */\n allowAttributes?: string[];\n}\n\nexport interface ContentConfig {\n loader: GlobLoader;\n schemas: { book: z.ZodType; chapter: z.ZodType };\n /** Applied to `.md` and `.mdx`. */\n remarkPlugins: PluginList;\n rehypePlugins: PluginList;\n sanitize: SanitizeOptions;\n /** Include chapters with `draft: true`. Defaults to `true` in development. */\n includeDrafts: boolean;\n /** Where compiled chapters are cached. */\n cacheDir: string;\n}\n\ntype DeepPartial<T> = T extends unknown[] ? T : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;\n\nexport interface ReaderUserConfig {\n content?: DeepPartial<Omit<ContentConfig, \"schemas\" | \"loader\">> & {\n loader?: GlobLoader;\n schemas?: Partial<ContentConfig[\"schemas\"]>;\n };\n reader?: DeepPartial<ReaderClientOptions>;\n}\n\nexport interface ResolvedReaderConfig {\n root: string;\n content: ContentConfig;\n reader: ReaderClientOptions;\n}\n\n// ------------------------------------------------------------- defaults\n\nexport const defaultBookSchema = z.looseObject({\n title: z.string(),\n subtitle: z.string().optional(),\n authors: z.array(z.string()).default([]),\n description: z.string().optional(),\n cover: z.string().optional(),\n /** Optional explicit chapter order (chapter slugs). */\n chapters: z.array(z.string()).optional(),\n});\n\nexport const defaultChapterSchema = z.looseObject({\n title: z.string(),\n order: z.number().optional(),\n description: z.string().optional(),\n part: z.string().optional(),\n label: z.string().optional(),\n number: z.union([z.string(), z.number()]).transform(String).optional(),\n draft: z.boolean().default(false),\n});\n\nfunction isDev() {\n return typeof process !== \"undefined\" && process.env?.NODE_ENV === \"development\";\n}\n\nexport function defaultContentConfig(): ContentConfig {\n return {\n loader: glob({ base: \"./content/books\" }),\n schemas: { book: defaultBookSchema, chapter: defaultChapterSchema },\n remarkPlugins: [],\n rehypePlugins: [],\n sanitize: {},\n includeDrafts: isDev(),\n cacheDir:\n (typeof process !== \"undefined\" && process.env?.ZREADER_CACHE_DIR) || \"node_modules/.cache/zreader\",\n };\n}\n\n// ------------------------------------------------------------ define/merge\n\n/** Identity helper that gives `reader.config.ts` full type-checking. */\nexport function defineConfig(config: ReaderUserConfig): ReaderUserConfig {\n return config;\n}\n\nfunction isPlainObject(x: unknown): x is Record<string, unknown> {\n if (typeof x !== \"object\" || x === null || Array.isArray(x)) return false;\n const proto = Object.getPrototypeOf(x);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Deep merge: plain objects merge key-by-key, arrays and class instances\n * (e.g. zod schemas) replace, `undefined` never overrides.\n */\nexport function mergeConfig<T>(base: T, ...overrides: unknown[]): T {\n let out: unknown = base;\n for (const o of overrides) out = mergeTwo(out, o);\n return out as T;\n}\n\nfunction mergeTwo(a: unknown, b: unknown): unknown {\n if (b === undefined) return a;\n if (isPlainObject(a) && isPlainObject(b)) {\n const out: Record<string, unknown> = { ...a };\n for (const [k, v] of Object.entries(b)) out[k] = mergeTwo(a[k], v);\n return out;\n }\n return b;\n}\n\n// --------------------------------------------------------------- validate\n\nconst themeName = z.enum(THEMES);\nconst highlightColor = z.enum(HIGHLIGHT_COLORS);\nconst clientSchema = z\n .object({\n storagePrefix: z.string().regex(/^[a-z0-9_-]+$/i, \"use letters, digits, '-' or '_'\"),\n highlightColors: z.array(highlightColor).min(1, \"must be non-empty\"),\n defaultHighlightColor: highlightColor,\n scratchpad: z.object({\n enabled: z.boolean(),\n scopes: z.array(z.enum([\"global\", \"book\", \"chapter\"])).min(1, \"must be non-empty\"),\n }),\n themes: z.array(themeName).min(1),\n defaultTheme: themeName,\n defaultSettings: z.object({\n textSize: z.enum(TEXT_SIZES),\n leading: z.enum(LEADINGS),\n measure: z.enum(MEASURES),\n fontFamily: z.enum(FONT_FAMILIES),\n highlightMode: z.boolean(),\n }),\n progress: z.object({ completeAt: z.number().gt(0).max(1), saveIntervalMs: z.number().min(100) }),\n backupReminder: z.boolean(),\n basePath: z.string().startsWith(\"/\"),\n })\n .refine((c) => c.highlightColors.includes(c.defaultHighlightColor), {\n message: \"defaultHighlightColor must be one of highlightColors\",\n path: [\"defaultHighlightColor\"],\n })\n .refine((c) => c.themes.includes(c.defaultTheme), {\n message: \"defaultTheme must be one of themes\",\n path: [\"defaultTheme\"],\n });\n\nfunction isSchemaLike(v: unknown): boolean {\n return typeof v === \"object\" && v !== null && typeof (v as { safeParse?: unknown }).safeParse === \"function\";\n}\n\nconst contentSchema = z.object({\n loader: z.object({\n kind: z.literal(\"glob\"),\n base: z.string().min(1),\n pattern: z.union([z.string(), z.array(z.string()).min(1)]),\n ignore: z.array(z.string()).optional(),\n }),\n schemas: z.object({\n book: z.custom<z.ZodType>(isSchemaLike, \"must be a zod (or safeParse-compatible) schema\"),\n chapter: z.custom<z.ZodType>(isSchemaLike, \"must be a zod (or safeParse-compatible) schema\"),\n }),\n remarkPlugins: z.array(z.any()),\n rehypePlugins: z.array(z.any()),\n sanitize: z.object({\n allowIframes: z.array(z.string()).optional(),\n allowTags: z.array(z.string()).optional(),\n allowAttributes: z.array(z.string()).optional(),\n }),\n includeDrafts: z.boolean(),\n cacheDir: z.string(),\n});\n\nexport class ReaderConfigError extends Error {\n constructor(\n message: string,\n public readonly issues: { path: string; message: string }[] = [],\n ) {\n super(message);\n this.name = \"ReaderConfigError\";\n }\n}\n\nfunction formatIssues(source: string, section: string, error: z.ZodError) {\n const issues = error.issues.map((i) => ({ path: [section, ...i.path.map(String)].join(\".\"), message: i.message }));\n return new ReaderConfigError(\n `Invalid reader config${source ? ` (${source})` : \"\"}:\\n` + issues.map((i) => ` - ${i.path}: ${i.message}`).join(\"\\n\"),\n issues,\n );\n}\n\n/**\n * Resolve the final config: package defaults ← reader.config.ts ← inline overrides.\n * Throws `ReaderConfigError` with readable messages.\n */\nexport function resolveConfig(\n user?: ReaderUserConfig | null,\n inline?: ReaderUserConfig | null,\n opts: { root?: string; source?: string } = {},\n): ResolvedReaderConfig {\n const root = opts.root ?? (typeof process !== \"undefined\" && process.cwd ? process.cwd() : \"/\");\n const reader = mergeConfig<ReaderClientOptions>(\n structuredClone(DEFAULT_CLIENT_OPTIONS),\n user?.reader,\n inline?.reader,\n );\n const content = mergeConfig<ContentConfig>(defaultContentConfig(), user?.content, inline?.content);\n\n const r = clientSchema.safeParse(reader);\n if (!r.success) throw formatIssues(opts.source ?? \"\", \"reader\", r.error);\n const c = contentSchema.safeParse(content);\n if (!c.success) throw formatIssues(opts.source ?? \"\", \"content\", c.error);\n\n return { root, content, reader };\n}\n\n/** Only the serializable, client-safe half. */\nexport function clientOptions(config: ResolvedReaderConfig): ReaderClientOptions {\n return config.reader;\n}\n\nexport const CONFIG_FILE_NAMES = [\"reader.config.ts\", \"reader.config.mts\", \"reader.config.mjs\", \"reader.config.js\"];\n"]}
|