zreader 0.0.0-stage → 0.2.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 +25 -0
- package/LICENSE +21 -0
- package/README.md +467 -2
- package/dist/agent/index.d.ts +90 -0
- package/dist/agent/index.js +293 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/react/index.d.ts +65 -0
- package/dist/agent/react/index.js +286 -0
- package/dist/agent/react/index.js.map +1 -0
- 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 +275 -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 +265 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/server/index.d.ts +81 -0
- package/dist/next/server/index.js +51 -0
- package/dist/next/server/index.js.map +1 -0
- package/dist/react/index.d.ts +392 -0
- package/dist/react/index.js +2798 -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 +42 -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 +237 -4
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
import { BLOCK_TAGS, createBlockIdAllocator, createSlugger, normalizeBlockText } from '../chunk-ISI35TCM.js';
|
|
2
|
+
export { BLOCK_TAGS, BLOCK_TAG_NAMES, createBlockIdAllocator, createSlugger, hashString, normalizeBlockText } from '../chunk-ISI35TCM.js';
|
|
3
|
+
import { DEFAULT_CLIENT_OPTIONS, FONT_FAMILIES, MEASURES, LEADINGS, TEXT_SIZES } from '../chunk-DYQNGV46.js';
|
|
4
|
+
export { DEFAULT_CLIENT_OPTIONS, FONT_FAMILIES, HIGHLIGHT_COLORS, LEADINGS, MEASURES, TEXT_SIZES, THEMES } from '../chunk-DYQNGV46.js';
|
|
5
|
+
import search from 'approx-string-match';
|
|
6
|
+
|
|
7
|
+
// src/core/storage.ts
|
|
8
|
+
var StorageQuotaError = class extends Error {
|
|
9
|
+
constructor(key, cause) {
|
|
10
|
+
super(`Storage quota exceeded while writing "${key}"`);
|
|
11
|
+
this.key = key;
|
|
12
|
+
this.name = "StorageQuotaError";
|
|
13
|
+
this.cause = cause;
|
|
14
|
+
}
|
|
15
|
+
key;
|
|
16
|
+
};
|
|
17
|
+
function isQuotaError(err) {
|
|
18
|
+
if (!err || typeof err !== "object") return false;
|
|
19
|
+
const e = err;
|
|
20
|
+
return e.name === "QuotaExceededError" || e.name === "NS_ERROR_DOM_QUOTA_REACHED" || e.code === 22 || e.code === 1014;
|
|
21
|
+
}
|
|
22
|
+
function createMemoryAdapter(initial = {}) {
|
|
23
|
+
const map = new Map(Object.entries(initial));
|
|
24
|
+
return {
|
|
25
|
+
get: (k) => map.get(k) ?? null,
|
|
26
|
+
set: (k, v) => void map.set(k, v),
|
|
27
|
+
remove: (k) => void map.delete(k),
|
|
28
|
+
keys: (prefix = "") => [...map.keys()].filter((k) => k.startsWith(prefix)),
|
|
29
|
+
subscribe: () => () => {
|
|
30
|
+
},
|
|
31
|
+
dump: () => Object.fromEntries(map)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function getLocalStorage() {
|
|
35
|
+
try {
|
|
36
|
+
if (typeof window === "undefined" || !window.localStorage) return null;
|
|
37
|
+
const probe = "__zr_probe__";
|
|
38
|
+
window.localStorage.setItem(probe, "1");
|
|
39
|
+
window.localStorage.removeItem(probe);
|
|
40
|
+
return window.localStorage;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function createLocalStorageAdapter(storage = getLocalStorage()) {
|
|
46
|
+
if (!storage) return createMemoryAdapter();
|
|
47
|
+
return {
|
|
48
|
+
get: (k) => {
|
|
49
|
+
try {
|
|
50
|
+
return storage.getItem(k);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
set: (k, v) => {
|
|
56
|
+
try {
|
|
57
|
+
storage.setItem(k, v);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (isQuotaError(err)) throw new StorageQuotaError(k, err);
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
remove: (k) => {
|
|
64
|
+
try {
|
|
65
|
+
storage.removeItem(k);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
keys: (prefix = "") => {
|
|
70
|
+
const out = [];
|
|
71
|
+
for (let i = 0; i < storage.length; i++) {
|
|
72
|
+
const k = storage.key(i);
|
|
73
|
+
if (k != null && k.startsWith(prefix)) out.push(k);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
},
|
|
77
|
+
subscribe: (listener) => {
|
|
78
|
+
if (typeof window === "undefined") return () => {
|
|
79
|
+
};
|
|
80
|
+
const onStorage = (e) => {
|
|
81
|
+
if (e.storageArea && e.storageArea !== storage) return;
|
|
82
|
+
listener(e.key);
|
|
83
|
+
};
|
|
84
|
+
window.addEventListener("storage", onStorage);
|
|
85
|
+
return () => window.removeEventListener("storage", onStorage);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async function requestPersistentStorage() {
|
|
90
|
+
try {
|
|
91
|
+
if (typeof navigator === "undefined" || !navigator.storage?.persist) return false;
|
|
92
|
+
if (await navigator.storage.persisted()) return true;
|
|
93
|
+
return await navigator.storage.persist();
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/core/schema.ts
|
|
100
|
+
var MIGRATIONS = {
|
|
101
|
+
settings: [],
|
|
102
|
+
highlights: [],
|
|
103
|
+
bookmarks: [],
|
|
104
|
+
progress: [],
|
|
105
|
+
pad: []
|
|
106
|
+
};
|
|
107
|
+
function currentVersion(type, migrations = MIGRATIONS) {
|
|
108
|
+
return migrations[type].length + 1;
|
|
109
|
+
}
|
|
110
|
+
function wrap(type, data, migrations = MIGRATIONS) {
|
|
111
|
+
return JSON.stringify({ v: currentVersion(type, migrations), data });
|
|
112
|
+
}
|
|
113
|
+
function unwrap(type, raw, migrations = MIGRATIONS) {
|
|
114
|
+
if (raw == null) return void 0;
|
|
115
|
+
let env;
|
|
116
|
+
try {
|
|
117
|
+
env = JSON.parse(raw);
|
|
118
|
+
} catch {
|
|
119
|
+
return void 0;
|
|
120
|
+
}
|
|
121
|
+
if (!isObj(env) || typeof env.v !== "number" || !("data" in env)) return void 0;
|
|
122
|
+
const target = currentVersion(type, migrations);
|
|
123
|
+
if (env.v > target) return void 0;
|
|
124
|
+
let data = env.data;
|
|
125
|
+
for (let v = env.v; v < target; v++) {
|
|
126
|
+
const m = migrations[type][v - 1];
|
|
127
|
+
if (!m) return void 0;
|
|
128
|
+
data = m(data);
|
|
129
|
+
}
|
|
130
|
+
return data;
|
|
131
|
+
}
|
|
132
|
+
function isObj(x) {
|
|
133
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
134
|
+
}
|
|
135
|
+
var str = (x) => typeof x === "string";
|
|
136
|
+
var num = (x) => typeof x === "number" && Number.isFinite(x);
|
|
137
|
+
var optStr = (x) => x === void 0 || str(x);
|
|
138
|
+
function isAnchor(x) {
|
|
139
|
+
return isObj(x) && str(x.blockId) && num(x.blockStart) && num(x.start) && num(x.end) && x.end >= x.start && str(x.exact) && str(x.prefix) && str(x.suffix) && optStr(x.contentHash);
|
|
140
|
+
}
|
|
141
|
+
function isHighlight(x) {
|
|
142
|
+
return isObj(x) && str(x.id) && str(x.bookId) && str(x.chapterId) && isAnchor(x.anchor) && str(x.color) && optStr(x.note) && str(x.createdAt) && str(x.updatedAt);
|
|
143
|
+
}
|
|
144
|
+
function isBookmark(x) {
|
|
145
|
+
return isObj(x) && str(x.id) && str(x.bookId) && str(x.chapterId) && optStr(x.blockId) && optStr(x.label) && optStr(x.excerpt) && str(x.createdAt) && str(x.updatedAt);
|
|
146
|
+
}
|
|
147
|
+
function isChapterProgress(x) {
|
|
148
|
+
return isObj(x) && str(x.chapterId) && optStr(x.blockId) && num(x.percent) && typeof x.completed === "boolean" && str(x.updatedAt);
|
|
149
|
+
}
|
|
150
|
+
function sanitizeProgress(x, bookId) {
|
|
151
|
+
const out = { bookId, chapters: {} };
|
|
152
|
+
if (!isObj(x)) return out;
|
|
153
|
+
if (isObj(x.chapters)) {
|
|
154
|
+
for (const [k, v] of Object.entries(x.chapters)) if (isChapterProgress(v)) out.chapters[k] = v;
|
|
155
|
+
}
|
|
156
|
+
if (isObj(x.last) && str(x.last.chapterId) && str(x.last.updatedAt) && optStr(x.last.blockId)) {
|
|
157
|
+
out.last = { chapterId: x.last.chapterId, blockId: x.last.blockId, updatedAt: x.last.updatedAt };
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
function sanitizeList(x, guard) {
|
|
162
|
+
return Array.isArray(x) ? x.filter(guard) : [];
|
|
163
|
+
}
|
|
164
|
+
function sanitizePad(x) {
|
|
165
|
+
if (!isObj(x) || !str(x.markdown) || !str(x.updatedAt)) return void 0;
|
|
166
|
+
return { markdown: x.markdown, json: x.json, updatedAt: x.updatedAt };
|
|
167
|
+
}
|
|
168
|
+
var oneOf = (list) => (v) => typeof v === "string" && list.includes(v);
|
|
169
|
+
function sanitizeSettings(x, defaults) {
|
|
170
|
+
if (!isObj(x)) return defaults;
|
|
171
|
+
const pick = (k, ok) => ok(x[k]) ? x[k] : defaults[k];
|
|
172
|
+
return {
|
|
173
|
+
textSize: pick("textSize", oneOf(TEXT_SIZES)),
|
|
174
|
+
leading: pick("leading", oneOf(LEADINGS)),
|
|
175
|
+
measure: pick("measure", oneOf(MEASURES)),
|
|
176
|
+
fontFamily: pick("fontFamily", oneOf(FONT_FAMILIES)),
|
|
177
|
+
highlightMode: pick("highlightMode", (v) => typeof v === "boolean"),
|
|
178
|
+
updatedAt: pick("updatedAt", str)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/core/store.ts
|
|
183
|
+
var enc = encodeURIComponent;
|
|
184
|
+
function padKeySuffix(scope) {
|
|
185
|
+
switch (scope.kind) {
|
|
186
|
+
case "global":
|
|
187
|
+
return "pad:global";
|
|
188
|
+
case "book":
|
|
189
|
+
return `pad:book:${enc(scope.bookId)}`;
|
|
190
|
+
case "chapter":
|
|
191
|
+
return `pad:chapter:${enc(scope.bookId)}:${enc(scope.chapterId)}`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function defaultId() {
|
|
195
|
+
const c = globalThis.crypto;
|
|
196
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
197
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
198
|
+
}
|
|
199
|
+
function createReaderStore(opts = {}) {
|
|
200
|
+
const adapter = opts.adapter ?? createLocalStorageAdapter();
|
|
201
|
+
const options = { ...DEFAULT_CLIENT_OPTIONS, ...opts.options };
|
|
202
|
+
const prefix = `${options.storagePrefix}:v1:`;
|
|
203
|
+
const writeDelay = opts.writeDelay ?? 200;
|
|
204
|
+
const onError = opts.onError ?? ((e) => console.warn("[zreader]", e));
|
|
205
|
+
const now = () => (opts.now ? opts.now() : /* @__PURE__ */ new Date()).toISOString();
|
|
206
|
+
const newId = opts.id ?? defaultId;
|
|
207
|
+
const defaultSettings = { ...options.defaultSettings, updatedAt: (/* @__PURE__ */ new Date(0)).toISOString() };
|
|
208
|
+
const cells = /* @__PURE__ */ new Map();
|
|
209
|
+
function cell(suffix, type, parse) {
|
|
210
|
+
const key = prefix + suffix;
|
|
211
|
+
const existing = cells.get(key);
|
|
212
|
+
if (existing) return existing;
|
|
213
|
+
let value = parse(unwrap(type, adapter.get(key)));
|
|
214
|
+
let timer;
|
|
215
|
+
let dirty = false;
|
|
216
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
217
|
+
const emit = () => listeners.forEach((l) => l());
|
|
218
|
+
const flush = () => {
|
|
219
|
+
if (timer) clearTimeout(timer);
|
|
220
|
+
timer = void 0;
|
|
221
|
+
if (!dirty) return;
|
|
222
|
+
dirty = false;
|
|
223
|
+
try {
|
|
224
|
+
adapter.set(key, wrap(type, value));
|
|
225
|
+
} catch (err) {
|
|
226
|
+
onError(err instanceof StorageQuotaError ? err : new StorageQuotaError(key, err));
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const c = {
|
|
230
|
+
key,
|
|
231
|
+
get: () => value,
|
|
232
|
+
set: (next) => {
|
|
233
|
+
const v = typeof next === "function" ? next(value) : next;
|
|
234
|
+
if (Object.is(v, value)) return;
|
|
235
|
+
value = v;
|
|
236
|
+
dirty = true;
|
|
237
|
+
emit();
|
|
238
|
+
if (writeDelay <= 0) flush();
|
|
239
|
+
else {
|
|
240
|
+
if (timer) clearTimeout(timer);
|
|
241
|
+
timer = setTimeout(flush, writeDelay);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
subscribe: (l) => {
|
|
245
|
+
listeners.add(l);
|
|
246
|
+
return () => listeners.delete(l);
|
|
247
|
+
},
|
|
248
|
+
reload: () => {
|
|
249
|
+
if (dirty) return;
|
|
250
|
+
value = parse(unwrap(type, adapter.get(key)));
|
|
251
|
+
emit();
|
|
252
|
+
},
|
|
253
|
+
flush
|
|
254
|
+
};
|
|
255
|
+
cells.set(key, c);
|
|
256
|
+
return c;
|
|
257
|
+
}
|
|
258
|
+
const flushAll = () => cells.forEach((c) => c.flush());
|
|
259
|
+
let connections = 0;
|
|
260
|
+
let disconnect;
|
|
261
|
+
function connect() {
|
|
262
|
+
if (connections++ === 0) {
|
|
263
|
+
const unsubscribeAdapter = adapter.subscribe((key) => {
|
|
264
|
+
if (key == null) cells.forEach((c) => c.reload());
|
|
265
|
+
else cells.get(key)?.reload();
|
|
266
|
+
});
|
|
267
|
+
const onHide = () => flushAll();
|
|
268
|
+
const onVisibility = () => {
|
|
269
|
+
if (document.visibilityState === "hidden") flushAll();
|
|
270
|
+
};
|
|
271
|
+
if (typeof window !== "undefined") {
|
|
272
|
+
window.addEventListener("pagehide", onHide);
|
|
273
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
274
|
+
}
|
|
275
|
+
disconnect = () => {
|
|
276
|
+
flushAll();
|
|
277
|
+
unsubscribeAdapter();
|
|
278
|
+
if (typeof window !== "undefined") {
|
|
279
|
+
window.removeEventListener("pagehide", onHide);
|
|
280
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
let done = false;
|
|
285
|
+
return () => {
|
|
286
|
+
if (done) return;
|
|
287
|
+
done = true;
|
|
288
|
+
if (--connections === 0) disconnect?.();
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const settings = () => cell("settings", "settings", (x) => sanitizeSettings(x, defaultSettings));
|
|
292
|
+
const highlights = (bookId) => cell(`book:${enc(bookId)}:highlights`, "highlights", (x) => sanitizeList(x, isHighlight));
|
|
293
|
+
const bookmarks = (bookId) => cell(`book:${enc(bookId)}:bookmarks`, "bookmarks", (x) => sanitizeList(x, isBookmark));
|
|
294
|
+
const progress = (bookId) => cell(`book:${enc(bookId)}:progress`, "progress", (x) => sanitizeProgress(x, bookId));
|
|
295
|
+
const emptyPad = { markdown: "", updatedAt: (/* @__PURE__ */ new Date(0)).toISOString() };
|
|
296
|
+
const pad = (scope) => cell(padKeySuffix(scope), "pad", (x) => sanitizePad(x) ?? emptyPad);
|
|
297
|
+
const emptyList = [];
|
|
298
|
+
const emptyProgress = /* @__PURE__ */ new Map();
|
|
299
|
+
const server = {
|
|
300
|
+
settings: defaultSettings,
|
|
301
|
+
list: emptyList,
|
|
302
|
+
pad: emptyPad,
|
|
303
|
+
progress(bookId) {
|
|
304
|
+
let p = emptyProgress.get(bookId);
|
|
305
|
+
if (!p) emptyProgress.set(bookId, p = { bookId, chapters: {} });
|
|
306
|
+
return p;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const api = {
|
|
310
|
+
options,
|
|
311
|
+
adapter,
|
|
312
|
+
server,
|
|
313
|
+
prefix,
|
|
314
|
+
settings,
|
|
315
|
+
highlights,
|
|
316
|
+
bookmarks,
|
|
317
|
+
progress,
|
|
318
|
+
pad,
|
|
319
|
+
updateSettings(patch) {
|
|
320
|
+
settings().set((s) => ({ ...s, ...patch, updatedAt: now() }));
|
|
321
|
+
},
|
|
322
|
+
resetSettings() {
|
|
323
|
+
settings().set({ ...defaultSettings, updatedAt: now() });
|
|
324
|
+
},
|
|
325
|
+
addHighlight(input) {
|
|
326
|
+
const t = now();
|
|
327
|
+
const h = {
|
|
328
|
+
id: newId(),
|
|
329
|
+
bookId: input.bookId,
|
|
330
|
+
chapterId: input.chapterId,
|
|
331
|
+
anchor: input.anchor,
|
|
332
|
+
color: input.color ?? options.defaultHighlightColor,
|
|
333
|
+
...input.note ? { note: input.note } : {},
|
|
334
|
+
createdAt: t,
|
|
335
|
+
updatedAt: t
|
|
336
|
+
};
|
|
337
|
+
highlights(input.bookId).set((list) => [...list, h]);
|
|
338
|
+
return h;
|
|
339
|
+
},
|
|
340
|
+
updateHighlight(bookId, id, patch) {
|
|
341
|
+
highlights(bookId).set(
|
|
342
|
+
(list) => list.map((h) => {
|
|
343
|
+
if (h.id !== id) return h;
|
|
344
|
+
const next = { ...h, ...patch, updatedAt: now() };
|
|
345
|
+
if (patch.note !== void 0 && !patch.note.trim()) delete next.note;
|
|
346
|
+
return next;
|
|
347
|
+
})
|
|
348
|
+
);
|
|
349
|
+
},
|
|
350
|
+
removeHighlight(bookId, id) {
|
|
351
|
+
highlights(bookId).set((list) => list.filter((h) => h.id !== id));
|
|
352
|
+
},
|
|
353
|
+
addBookmark(input) {
|
|
354
|
+
const t = now();
|
|
355
|
+
const b = { ...input, id: newId(), createdAt: t, updatedAt: t };
|
|
356
|
+
bookmarks(input.bookId).set((list) => [...list, b]);
|
|
357
|
+
return b;
|
|
358
|
+
},
|
|
359
|
+
removeBookmark(bookId, id) {
|
|
360
|
+
bookmarks(bookId).set((list) => list.filter((b) => b.id !== id));
|
|
361
|
+
},
|
|
362
|
+
findChapterBookmark(bookId, chapterId) {
|
|
363
|
+
return bookmarks(bookId).get().find((b) => b.chapterId === chapterId && !b.blockId);
|
|
364
|
+
},
|
|
365
|
+
/** Toggle a whole-chapter bookmark. Returns whether the chapter is now bookmarked. */
|
|
366
|
+
toggleChapterBookmark(bookId, chapterId, label) {
|
|
367
|
+
const existing = api.findChapterBookmark(bookId, chapterId);
|
|
368
|
+
if (existing) {
|
|
369
|
+
api.removeBookmark(bookId, existing.id);
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
api.addBookmark({ bookId, chapterId, ...label ? { label } : {} });
|
|
373
|
+
return true;
|
|
374
|
+
},
|
|
375
|
+
recordProgress(bookId, chapterId, p) {
|
|
376
|
+
const t = now();
|
|
377
|
+
const percent = Math.max(0, Math.min(1, p.percent));
|
|
378
|
+
progress(bookId).set((prev) => {
|
|
379
|
+
const old = prev.chapters[chapterId];
|
|
380
|
+
const completed = (old?.completed ?? false) || percent >= options.progress.completeAt;
|
|
381
|
+
if (old && old.blockId === p.blockId && Math.abs(old.percent - percent) < 5e-3 && old.completed === completed) {
|
|
382
|
+
return prev;
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
...prev,
|
|
386
|
+
chapters: {
|
|
387
|
+
...prev.chapters,
|
|
388
|
+
[chapterId]: { chapterId, blockId: p.blockId, percent, completed, updatedAt: t }
|
|
389
|
+
},
|
|
390
|
+
last: { chapterId, blockId: p.blockId, updatedAt: t }
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
markChapterComplete(bookId, chapterId, completed = true) {
|
|
395
|
+
const t = now();
|
|
396
|
+
progress(bookId).set((prev) => {
|
|
397
|
+
const old = prev.chapters[chapterId];
|
|
398
|
+
return {
|
|
399
|
+
...prev,
|
|
400
|
+
chapters: {
|
|
401
|
+
...prev.chapters,
|
|
402
|
+
[chapterId]: {
|
|
403
|
+
chapterId,
|
|
404
|
+
blockId: old?.blockId,
|
|
405
|
+
percent: completed ? 1 : old?.percent ?? 0,
|
|
406
|
+
completed,
|
|
407
|
+
updatedAt: t
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
},
|
|
413
|
+
savePad(scope, markdown, json) {
|
|
414
|
+
pad(scope).set({ markdown, ...json !== void 0 ? { json } : {}, updatedAt: now() });
|
|
415
|
+
},
|
|
416
|
+
/** Append markdown to a pad from outside the editor (drops the JSON cache so the editor re-parses). */
|
|
417
|
+
appendToPad(scope, markdown) {
|
|
418
|
+
pad(scope).set((p) => ({
|
|
419
|
+
markdown: p.markdown.trim() ? `${p.markdown.replace(/\s+$/, "")}
|
|
420
|
+
|
|
421
|
+
${markdown}
|
|
422
|
+
` : `${markdown}
|
|
423
|
+
`,
|
|
424
|
+
updatedAt: now()
|
|
425
|
+
}));
|
|
426
|
+
},
|
|
427
|
+
clearPad(scope) {
|
|
428
|
+
adapter.remove(prefix + padKeySuffix(scope));
|
|
429
|
+
cells.get(prefix + padKeySuffix(scope))?.reload();
|
|
430
|
+
},
|
|
431
|
+
/** All storage keys owned by this store's prefix. */
|
|
432
|
+
keys() {
|
|
433
|
+
return adapter.keys(prefix);
|
|
434
|
+
},
|
|
435
|
+
flush: flushAll,
|
|
436
|
+
connect,
|
|
437
|
+
/** Drop cached cells so the next read comes from storage (used after imports). */
|
|
438
|
+
reloadAll() {
|
|
439
|
+
cells.forEach((c) => {
|
|
440
|
+
c.flush();
|
|
441
|
+
c.reload();
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
return api;
|
|
446
|
+
}
|
|
447
|
+
function quoteToMarkdown(text, opts = {}) {
|
|
448
|
+
const quote = text.trim().split(/\r?\n/).map((l) => `> ${l}`).join("\n");
|
|
449
|
+
const link = opts.href ? `
|
|
450
|
+
>
|
|
451
|
+
> \u2014 [source](${opts.href})` : "";
|
|
452
|
+
const note = opts.note ? `
|
|
453
|
+
|
|
454
|
+
${opts.note}` : "";
|
|
455
|
+
return `${quote}${link}${note}`;
|
|
456
|
+
}
|
|
457
|
+
function highlightToMarkdown(h, href) {
|
|
458
|
+
return quoteToMarkdown(h.anchor.exact, { href, note: h.note });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/core/backup.ts
|
|
462
|
+
var BACKUP_FORMAT = "zreader-backup";
|
|
463
|
+
var LEGACY_BACKUP_FORMATS = ["alr-backup"];
|
|
464
|
+
var BACKUP_VERSION = 1;
|
|
465
|
+
function recordTypeForKey(key) {
|
|
466
|
+
if (key === "settings") return "settings";
|
|
467
|
+
if (key.startsWith("pad:")) return "pad";
|
|
468
|
+
const m = /^book:[^:]+:(highlights|bookmarks|progress)$/.exec(key);
|
|
469
|
+
return m?.[1] ?? null;
|
|
470
|
+
}
|
|
471
|
+
function bookIdFromKey(key) {
|
|
472
|
+
return decodeURIComponent(key.split(":")[1] ?? "");
|
|
473
|
+
}
|
|
474
|
+
function exportBackup(store, opts = {}) {
|
|
475
|
+
store.flush();
|
|
476
|
+
const records = {};
|
|
477
|
+
for (const fullKey of store.keys()) {
|
|
478
|
+
const key = fullKey.slice(store.prefix.length);
|
|
479
|
+
if (!recordTypeForKey(key)) continue;
|
|
480
|
+
if (opts.bookId) {
|
|
481
|
+
const enc2 = encodeURIComponent(opts.bookId);
|
|
482
|
+
if (!key.startsWith(`book:${enc2}:`) && !key.startsWith(`pad:book:${enc2}`) && !key.startsWith(`pad:chapter:${enc2}:`))
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
const env = JSON.parse(store.adapter.get(fullKey) ?? "null");
|
|
487
|
+
if (isObj(env) && typeof env.v === "number") records[key] = env;
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return { format: BACKUP_FORMAT, schemaVersion: BACKUP_VERSION, exportedAt: (/* @__PURE__ */ new Date()).toISOString(), records };
|
|
492
|
+
}
|
|
493
|
+
var BackupFormatError = class extends Error {
|
|
494
|
+
constructor(message) {
|
|
495
|
+
super(message);
|
|
496
|
+
this.name = "BackupFormatError";
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
function parseBackup(input) {
|
|
500
|
+
let data = input;
|
|
501
|
+
if (typeof input === "string") {
|
|
502
|
+
try {
|
|
503
|
+
data = JSON.parse(input);
|
|
504
|
+
} catch {
|
|
505
|
+
throw new BackupFormatError("Backup is not valid JSON");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (!isObj(data) || data.format !== BACKUP_FORMAT && !LEGACY_BACKUP_FORMATS.includes(String(data.format)))
|
|
509
|
+
throw new BackupFormatError("Not a zreader backup");
|
|
510
|
+
if (typeof data.schemaVersion !== "number" || data.schemaVersion > BACKUP_VERSION)
|
|
511
|
+
throw new BackupFormatError("Backup was created by a newer version of the reader");
|
|
512
|
+
if (!isObj(data.records)) throw new BackupFormatError("Backup has no records");
|
|
513
|
+
return data;
|
|
514
|
+
}
|
|
515
|
+
function newer(a, b) {
|
|
516
|
+
return a.updatedAt >= b.updatedAt ? a : b;
|
|
517
|
+
}
|
|
518
|
+
function mergeById(local, incoming) {
|
|
519
|
+
const map = new Map(local.map((x) => [x.id, x]));
|
|
520
|
+
for (const x of incoming) {
|
|
521
|
+
const cur = map.get(x.id);
|
|
522
|
+
map.set(x.id, cur ? newer(cur, x) : x);
|
|
523
|
+
}
|
|
524
|
+
return [...map.values()].sort((a, b) => a.id < b.id ? -1 : 1);
|
|
525
|
+
}
|
|
526
|
+
function mergeProgress(local, incoming) {
|
|
527
|
+
const chapters = { ...local.chapters };
|
|
528
|
+
for (const [k, v] of Object.entries(incoming.chapters)) {
|
|
529
|
+
const cur = chapters[k];
|
|
530
|
+
chapters[k] = cur ? newer(cur, v) : v;
|
|
531
|
+
}
|
|
532
|
+
const last = local.last && incoming.last ? newer(local.last, incoming.last) : local.last ?? incoming.last;
|
|
533
|
+
return { bookId: local.bookId, chapters, ...last ? { last } : {} };
|
|
534
|
+
}
|
|
535
|
+
function importBackup(store, input, opts = {}) {
|
|
536
|
+
const backup = parseBackup(input);
|
|
537
|
+
const mode = opts.mode ?? "merge";
|
|
538
|
+
store.flush();
|
|
539
|
+
if (mode === "replace") for (const k of store.keys()) store.adapter.remove(k);
|
|
540
|
+
let imported = 0;
|
|
541
|
+
let skipped = 0;
|
|
542
|
+
const defaults = sanitizeSettings(void 0, store.settings().get());
|
|
543
|
+
for (const [key, env] of Object.entries(backup.records)) {
|
|
544
|
+
const type = recordTypeForKey(key);
|
|
545
|
+
if (!type) {
|
|
546
|
+
skipped++;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const data = unwrap(type, JSON.stringify(env));
|
|
550
|
+
if (data === void 0) {
|
|
551
|
+
skipped++;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const fullKey = store.prefix + key;
|
|
555
|
+
const localRaw = mode === "merge" ? unwrap(type, store.adapter.get(fullKey)) : void 0;
|
|
556
|
+
let out;
|
|
557
|
+
switch (type) {
|
|
558
|
+
case "highlights":
|
|
559
|
+
out = mergeById(sanitizeList(localRaw, isHighlight), sanitizeList(data, isHighlight));
|
|
560
|
+
break;
|
|
561
|
+
case "bookmarks":
|
|
562
|
+
out = mergeById(sanitizeList(localRaw, isBookmark), sanitizeList(data, isBookmark));
|
|
563
|
+
break;
|
|
564
|
+
case "progress": {
|
|
565
|
+
const id = bookIdFromKey(key);
|
|
566
|
+
out = mergeProgress(sanitizeProgress(localRaw, id), sanitizeProgress(data, id));
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
case "pad": {
|
|
570
|
+
const inc = sanitizePad(data);
|
|
571
|
+
const loc = sanitizePad(localRaw);
|
|
572
|
+
if (!inc) {
|
|
573
|
+
skipped++;
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
out = loc ? newer(loc, inc) : inc;
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
case "settings": {
|
|
580
|
+
const inc = sanitizeSettings(data, defaults);
|
|
581
|
+
out = localRaw === void 0 ? inc : newer(sanitizeSettings(localRaw, defaults), inc);
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
store.adapter.set(fullKey, wrap(type, out));
|
|
587
|
+
imported++;
|
|
588
|
+
} catch {
|
|
589
|
+
skipped++;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
store.reloadAll();
|
|
593
|
+
return { imported, skipped };
|
|
594
|
+
}
|
|
595
|
+
function highlightsToMarkdown(title, highlights, chapterTitle = (c) => c) {
|
|
596
|
+
const byChapter = /* @__PURE__ */ new Map();
|
|
597
|
+
for (const h of highlights) {
|
|
598
|
+
const list = byChapter.get(h.chapterId) ?? [];
|
|
599
|
+
list.push(h);
|
|
600
|
+
byChapter.set(h.chapterId, list);
|
|
601
|
+
}
|
|
602
|
+
const out = [`# ${title} \u2014 highlights`, ""];
|
|
603
|
+
for (const [chapterId, list] of byChapter) {
|
|
604
|
+
out.push(`## ${chapterTitle(chapterId)}`, "");
|
|
605
|
+
for (const h of [...list].sort((a, b) => a.anchor.start - b.anchor.start)) {
|
|
606
|
+
out.push(...h.anchor.exact.trim().split(/\r?\n/).map((l) => `> ${l}`));
|
|
607
|
+
if (h.note) out.push("", h.note);
|
|
608
|
+
out.push("");
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return out.join("\n");
|
|
612
|
+
}
|
|
613
|
+
var CONTEXT_LENGTH = 32;
|
|
614
|
+
function quoteFor(text, start, end) {
|
|
615
|
+
return {
|
|
616
|
+
exact: text.slice(start, end),
|
|
617
|
+
prefix: text.slice(Math.max(0, start - CONTEXT_LENGTH), start),
|
|
618
|
+
suffix: text.slice(end, end + CONTEXT_LENGTH)
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function commonSuffixLen(a, b) {
|
|
622
|
+
let i = 0;
|
|
623
|
+
while (i < a.length && i < b.length && a[a.length - 1 - i] === b[b.length - 1 - i]) i++;
|
|
624
|
+
return i;
|
|
625
|
+
}
|
|
626
|
+
function commonPrefixLen(a, b) {
|
|
627
|
+
let i = 0;
|
|
628
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
629
|
+
return i;
|
|
630
|
+
}
|
|
631
|
+
function score(text, anchor, start, end, errors = 0) {
|
|
632
|
+
const prefix = text.slice(Math.max(0, start - CONTEXT_LENGTH), start);
|
|
633
|
+
const suffix = text.slice(end, end + CONTEXT_LENGTH);
|
|
634
|
+
const ctx = commonSuffixLen(prefix, anchor.prefix) + commonPrefixLen(suffix, anchor.suffix);
|
|
635
|
+
const distance = Math.abs(start - anchor.start) / Math.max(1, text.length);
|
|
636
|
+
return ctx * 2 - errors * 4 - distance * 10;
|
|
637
|
+
}
|
|
638
|
+
function resolveInText(text, anchor, blockOffset) {
|
|
639
|
+
const { exact } = anchor;
|
|
640
|
+
if (!exact) return null;
|
|
641
|
+
if (text.slice(anchor.start, anchor.end) === exact) return { start: anchor.start, end: anchor.end, strategy: "position" };
|
|
642
|
+
if (blockOffset !== void 0) {
|
|
643
|
+
const s = blockOffset + anchor.blockStart;
|
|
644
|
+
if (text.slice(s, s + exact.length) === exact) return { start: s, end: s + exact.length, strategy: "block" };
|
|
645
|
+
}
|
|
646
|
+
let best = null;
|
|
647
|
+
for (let i = text.indexOf(exact); i !== -1; i = text.indexOf(exact, i + 1)) {
|
|
648
|
+
const sc = score(text, anchor, i, i + exact.length);
|
|
649
|
+
if (!best || sc > best.score) best = { start: i, end: i + exact.length, score: sc };
|
|
650
|
+
}
|
|
651
|
+
if (best) return { start: best.start, end: best.end, strategy: "quote" };
|
|
652
|
+
if (exact.length < 8) return null;
|
|
653
|
+
const maxErrors = Math.min(64, Math.floor(exact.length * 0.15));
|
|
654
|
+
const matches = search(text, exact, maxErrors);
|
|
655
|
+
let fuzzy = null;
|
|
656
|
+
for (const m of matches) {
|
|
657
|
+
const sc = score(text, anchor, m.start, m.end, m.errors);
|
|
658
|
+
if (!fuzzy || sc > fuzzy.score) fuzzy = { start: m.start, end: m.end, score: sc };
|
|
659
|
+
}
|
|
660
|
+
return fuzzy ? { start: fuzzy.start, end: fuzzy.end, strategy: "fuzzy" } : null;
|
|
661
|
+
}
|
|
662
|
+
function segmentSentences(text, locale) {
|
|
663
|
+
const Seg = Intl.Segmenter;
|
|
664
|
+
if (Seg) {
|
|
665
|
+
return [...new Seg(locale, { granularity: "sentence" }).segment(text)].map((s) => ({
|
|
666
|
+
index: s.index,
|
|
667
|
+
segment: s.segment
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
const out = [];
|
|
671
|
+
const re = /[^.!?…]*(?:[.!?…]+["'”’)\]]*|$)\s*/g;
|
|
672
|
+
let m;
|
|
673
|
+
while ((m = re.exec(text)) && m[0]) {
|
|
674
|
+
out.push({ index: m.index, segment: m[0] });
|
|
675
|
+
}
|
|
676
|
+
return out;
|
|
677
|
+
}
|
|
678
|
+
function trimSpan(text, start, end) {
|
|
679
|
+
while (start < end && /\s/.test(text[start] ?? "")) start++;
|
|
680
|
+
while (end > start && /\s/.test(text[end - 1] ?? "")) end--;
|
|
681
|
+
return { start, end };
|
|
682
|
+
}
|
|
683
|
+
function sentenceAt(blockText, offset, locale) {
|
|
684
|
+
const segs = segmentSentences(blockText, locale);
|
|
685
|
+
const clamped = Math.max(0, Math.min(offset, Math.max(0, blockText.length - 1)));
|
|
686
|
+
for (const s of segs) {
|
|
687
|
+
if (clamped >= s.index && clamped < s.index + s.segment.length) {
|
|
688
|
+
return trimSpan(blockText, s.index, s.index + s.segment.length);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return trimSpan(blockText, 0, blockText.length);
|
|
692
|
+
}
|
|
693
|
+
function expandToSentences(blockText, start, end, locale) {
|
|
694
|
+
const a = sentenceAt(blockText, start, locale);
|
|
695
|
+
const b = sentenceAt(blockText, Math.max(start, end - 1), locale);
|
|
696
|
+
return { start: Math.min(a.start, b.start), end: Math.max(a.end, b.end) };
|
|
697
|
+
}
|
|
698
|
+
var WORD = /[\p{L}\p{N}_'’-]/u;
|
|
699
|
+
function snapToWords(text, start, end) {
|
|
700
|
+
while (start > 0 && WORD.test(text[start - 1] ?? "") && WORD.test(text[start] ?? "")) start--;
|
|
701
|
+
while (end < text.length && WORD.test(text[end] ?? "") && WORD.test(text[end - 1] ?? "")) end++;
|
|
702
|
+
return trimSpan(text, start, end);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/core/dom.ts
|
|
706
|
+
var BLOCK_ATTR = "data-block-id";
|
|
707
|
+
var IGNORE_ATTR = "data-zr-ignore";
|
|
708
|
+
var BLOCK_SELECTOR = Array.from(BLOCK_TAGS).join(",");
|
|
709
|
+
var HEADING_TAG = /^H([1-6])$/;
|
|
710
|
+
function ensureBlockIds(root) {
|
|
711
|
+
if (root.querySelector(`[${BLOCK_ATTR}]`)) return { assigned: 0 };
|
|
712
|
+
const allocate = createBlockIdAllocator();
|
|
713
|
+
const slugger = createSlugger();
|
|
714
|
+
let assigned = 0;
|
|
715
|
+
for (const el of Array.from(root.querySelectorAll(BLOCK_SELECTOR))) {
|
|
716
|
+
const text = normalizeBlockText(el.textContent ?? "");
|
|
717
|
+
if (HEADING_TAG.test(el.tagName)) {
|
|
718
|
+
const existing = el.getAttribute("id");
|
|
719
|
+
if (existing) slugger.keep(existing);
|
|
720
|
+
else el.setAttribute("id", slugger.slug(text));
|
|
721
|
+
}
|
|
722
|
+
if (el.querySelector(BLOCK_SELECTOR)) continue;
|
|
723
|
+
if (!text) continue;
|
|
724
|
+
el.setAttribute(BLOCK_ATTR, allocate(el.tagName, text));
|
|
725
|
+
assigned++;
|
|
726
|
+
}
|
|
727
|
+
return { assigned };
|
|
728
|
+
}
|
|
729
|
+
function collectToc(root, depth = [2, 3]) {
|
|
730
|
+
const out = [];
|
|
731
|
+
for (const el of Array.from(root.querySelectorAll("h1,h2,h3,h4,h5,h6"))) {
|
|
732
|
+
const d = Number(HEADING_TAG.exec(el.tagName)?.[1] ?? 0);
|
|
733
|
+
const id = el.getAttribute("id");
|
|
734
|
+
const text = normalizeBlockText(el.textContent ?? "");
|
|
735
|
+
if (id && text && d >= depth[0] && d <= depth[1]) out.push({ id, depth: d, text });
|
|
736
|
+
}
|
|
737
|
+
return out;
|
|
738
|
+
}
|
|
739
|
+
function buildTextIndex(root) {
|
|
740
|
+
const entries = [];
|
|
741
|
+
let text = "";
|
|
742
|
+
const doc = root.ownerDocument;
|
|
743
|
+
const walker = doc.createTreeWalker(root, 4, {
|
|
744
|
+
acceptNode(node) {
|
|
745
|
+
const parent = node.parentElement;
|
|
746
|
+
if (!parent) return 2;
|
|
747
|
+
if (parent.closest(`[${IGNORE_ATTR}],script,style,noscript,template`)) return 2;
|
|
748
|
+
return 1;
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
752
|
+
const t = n;
|
|
753
|
+
const len = t.data.length;
|
|
754
|
+
if (!len) continue;
|
|
755
|
+
entries.push({ node: t, start: text.length, end: text.length + len });
|
|
756
|
+
text += t.data;
|
|
757
|
+
}
|
|
758
|
+
return { root, text, entries };
|
|
759
|
+
}
|
|
760
|
+
function pointToOffset(index, container, offset) {
|
|
761
|
+
if (container.nodeType === 3) {
|
|
762
|
+
const e = index.entries.find((x) => x.node === container);
|
|
763
|
+
if (e) return e.start + Math.min(offset, e.end - e.start);
|
|
764
|
+
}
|
|
765
|
+
const doc = index.root.ownerDocument;
|
|
766
|
+
const probe = doc.createRange();
|
|
767
|
+
probe.setStart(container, offset);
|
|
768
|
+
probe.collapse(true);
|
|
769
|
+
let lo = 0;
|
|
770
|
+
let hi = index.entries.length;
|
|
771
|
+
while (lo < hi) {
|
|
772
|
+
const mid = lo + hi >> 1;
|
|
773
|
+
const node = index.entries[mid].node;
|
|
774
|
+
let cmp;
|
|
775
|
+
try {
|
|
776
|
+
cmp = probe.comparePoint(node, 0);
|
|
777
|
+
} catch {
|
|
778
|
+
cmp = 1;
|
|
779
|
+
}
|
|
780
|
+
if (cmp < 0 && !(container.nodeType === 3 && node === container)) lo = mid + 1;
|
|
781
|
+
else hi = mid;
|
|
782
|
+
}
|
|
783
|
+
return lo < index.entries.length ? index.entries[lo].start : index.text.length;
|
|
784
|
+
}
|
|
785
|
+
function offsetToPoint(index, offset, preferNext = false) {
|
|
786
|
+
const { entries } = index;
|
|
787
|
+
if (!entries.length) return null;
|
|
788
|
+
let lo = 0;
|
|
789
|
+
let hi = entries.length - 1;
|
|
790
|
+
while (lo < hi) {
|
|
791
|
+
const mid = lo + hi >> 1;
|
|
792
|
+
if (entries[mid].end < offset || preferNext && entries[mid].end === offset) lo = mid + 1;
|
|
793
|
+
else hi = mid;
|
|
794
|
+
}
|
|
795
|
+
const e = entries[lo];
|
|
796
|
+
return { node: e.node, offset: Math.max(0, Math.min(offset - e.start, e.end - e.start)) };
|
|
797
|
+
}
|
|
798
|
+
function rangeToSpan(index, range) {
|
|
799
|
+
if (!index.root.contains(range.commonAncestorContainer) && range.commonAncestorContainer !== index.root) {
|
|
800
|
+
const r = range.cloneRange();
|
|
801
|
+
try {
|
|
802
|
+
if (!index.root.contains(r.startContainer)) r.setStart(index.root, 0);
|
|
803
|
+
if (!index.root.contains(r.endContainer)) r.setEnd(index.root, index.root.childNodes.length);
|
|
804
|
+
} catch {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
range = r;
|
|
808
|
+
}
|
|
809
|
+
const start = pointToOffset(index, range.startContainer, range.startOffset);
|
|
810
|
+
const end = pointToOffset(index, range.endContainer, range.endOffset);
|
|
811
|
+
return end > start ? { start, end } : null;
|
|
812
|
+
}
|
|
813
|
+
function spanToRange(index, span) {
|
|
814
|
+
const a = offsetToPoint(index, span.start, true);
|
|
815
|
+
const b = offsetToPoint(index, span.end);
|
|
816
|
+
if (!a || !b) return null;
|
|
817
|
+
const r = index.root.ownerDocument.createRange();
|
|
818
|
+
r.setStart(a.node, a.offset);
|
|
819
|
+
r.setEnd(b.node, b.offset);
|
|
820
|
+
return r;
|
|
821
|
+
}
|
|
822
|
+
function blockElementAt(index, offset) {
|
|
823
|
+
const p = offsetToPoint(index, offset, true);
|
|
824
|
+
return p?.node.parentElement?.closest(`[${BLOCK_ATTR}]`) ?? null;
|
|
825
|
+
}
|
|
826
|
+
function blockSpan(index, el) {
|
|
827
|
+
let start = -1;
|
|
828
|
+
let end = -1;
|
|
829
|
+
for (const e of index.entries) {
|
|
830
|
+
if (el.contains(e.node)) {
|
|
831
|
+
if (start < 0) start = e.start;
|
|
832
|
+
end = e.end;
|
|
833
|
+
} else if (start >= 0) break;
|
|
834
|
+
}
|
|
835
|
+
return start < 0 ? null : { start, end };
|
|
836
|
+
}
|
|
837
|
+
function findBlock(root, blockId) {
|
|
838
|
+
const esc = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(blockId) : blockId.replace(/"/g, '\\"');
|
|
839
|
+
return root.querySelector(`[${BLOCK_ATTR}="${esc}"]`);
|
|
840
|
+
}
|
|
841
|
+
function describeSpan(index, span, contentHash) {
|
|
842
|
+
const block = blockElementAt(index, span.start);
|
|
843
|
+
if (!block) return null;
|
|
844
|
+
const bs = blockSpan(index, block);
|
|
845
|
+
const q = quoteFor(index.text, span.start, span.end);
|
|
846
|
+
return {
|
|
847
|
+
blockId: block.getAttribute(BLOCK_ATTR) ?? "",
|
|
848
|
+
blockStart: span.start - (bs?.start ?? 0),
|
|
849
|
+
start: span.start,
|
|
850
|
+
end: span.end,
|
|
851
|
+
...q,
|
|
852
|
+
...contentHash ? { contentHash } : {}
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function resolveAnchor(index, anchor) {
|
|
856
|
+
const block = anchor.blockId ? findBlock(index.root, anchor.blockId) : null;
|
|
857
|
+
const bs = block ? blockSpan(index, block) : null;
|
|
858
|
+
return resolveInText(index.text, anchor, bs?.start);
|
|
859
|
+
}
|
|
860
|
+
function selectionSpan(index, selection) {
|
|
861
|
+
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return null;
|
|
862
|
+
const range = selection.getRangeAt(0);
|
|
863
|
+
if (!index.root.contains(range.commonAncestorContainer) && !range.intersectsNode(index.root)) return null;
|
|
864
|
+
const span = rangeToSpan(index, range);
|
|
865
|
+
if (!span) return null;
|
|
866
|
+
const snapped = snapToWords(index.text, span.start, span.end);
|
|
867
|
+
return snapped.end > snapped.start ? snapped : null;
|
|
868
|
+
}
|
|
869
|
+
function sentenceSpanAt(index, offset, locale) {
|
|
870
|
+
const block = blockElementAt(index, offset);
|
|
871
|
+
const bs = block ? blockSpan(index, block) : { start: 0, end: index.text.length };
|
|
872
|
+
if (!bs) return null;
|
|
873
|
+
const local = sentenceAt(index.text.slice(bs.start, bs.end), offset - bs.start, locale);
|
|
874
|
+
return local.end > local.start ? { start: bs.start + local.start, end: bs.start + local.end } : null;
|
|
875
|
+
}
|
|
876
|
+
function expandSpanToSentences(index, span, locale) {
|
|
877
|
+
const a = sentenceSpanAt(index, span.start, locale);
|
|
878
|
+
const b = sentenceSpanAt(index, Math.max(span.start, span.end - 1), locale);
|
|
879
|
+
return { start: Math.min(a?.start ?? span.start, b?.start ?? span.start), end: Math.max(a?.end ?? span.end, b?.end ?? span.end) };
|
|
880
|
+
}
|
|
881
|
+
function offsetFromPoint(index, x, y) {
|
|
882
|
+
const doc = index.root.ownerDocument;
|
|
883
|
+
let node = null;
|
|
884
|
+
let off = 0;
|
|
885
|
+
if (doc.caretPositionFromPoint) {
|
|
886
|
+
const p = doc.caretPositionFromPoint(x, y);
|
|
887
|
+
if (p) {
|
|
888
|
+
node = p.offsetNode;
|
|
889
|
+
off = p.offset;
|
|
890
|
+
}
|
|
891
|
+
} else if (doc.caretRangeFromPoint) {
|
|
892
|
+
const r = doc.caretRangeFromPoint(x, y);
|
|
893
|
+
if (r) {
|
|
894
|
+
node = r.startContainer;
|
|
895
|
+
off = r.startOffset;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (!node || !index.root.contains(node)) return null;
|
|
899
|
+
return pointToOffset(index, node, off);
|
|
900
|
+
}
|
|
901
|
+
function supportsHighlightApi() {
|
|
902
|
+
return typeof CSS !== "undefined" && "highlights" in CSS && typeof globalThis.Highlight === "function";
|
|
903
|
+
}
|
|
904
|
+
function highlightName(color, namespace = "zr") {
|
|
905
|
+
return `${namespace}-${color}`;
|
|
906
|
+
}
|
|
907
|
+
function paintHighlights(resolved, opts = {}) {
|
|
908
|
+
if (!supportsHighlightApi()) return () => {
|
|
909
|
+
};
|
|
910
|
+
const ns = opts.namespace ?? "zr";
|
|
911
|
+
const byName = /* @__PURE__ */ new Map();
|
|
912
|
+
for (const r of resolved) {
|
|
913
|
+
const name = highlightName(r.highlight.id === opts.activeId ? "active" : r.highlight.color, ns);
|
|
914
|
+
const list = byName.get(name) ?? [];
|
|
915
|
+
list.push(r.range);
|
|
916
|
+
byName.set(name, list);
|
|
917
|
+
}
|
|
918
|
+
const registry = CSS.highlights;
|
|
919
|
+
const Ctor = globalThis.Highlight;
|
|
920
|
+
for (const [name, ranges] of byName) registry.set(name, new Ctor(...ranges));
|
|
921
|
+
return () => byName.forEach((_, name) => registry.delete(name));
|
|
922
|
+
}
|
|
923
|
+
function highlightAtOffset(resolved, offset) {
|
|
924
|
+
let best = null;
|
|
925
|
+
for (const r of resolved) {
|
|
926
|
+
if (offset >= r.span.start && offset < r.span.end) {
|
|
927
|
+
if (!best || r.span.end - r.span.start < best.span.end - best.span.start) best = r;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return best;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
export { BACKUP_FORMAT, BACKUP_VERSION, BLOCK_ATTR, BackupFormatError, CONTEXT_LENGTH, IGNORE_ATTR, LEGACY_BACKUP_FORMATS, MIGRATIONS, StorageQuotaError, blockElementAt, blockSpan, buildTextIndex, collectToc, createLocalStorageAdapter, createMemoryAdapter, createReaderStore, currentVersion, defaultId, describeSpan, ensureBlockIds, expandSpanToSentences, expandToSentences, exportBackup, findBlock, getLocalStorage, highlightAtOffset, highlightName, highlightToMarkdown, highlightsToMarkdown, importBackup, isAnchor, isBookmark, isHighlight, offsetFromPoint, offsetToPoint, padKeySuffix, paintHighlights, parseBackup, pointToOffset, quoteFor, quoteToMarkdown, rangeToSpan, recordTypeForKey, requestPersistentStorage, resolveAnchor, resolveInText, selectionSpan, sentenceAt, sentenceSpanAt, snapToWords, spanToRange, supportsHighlightApi, unwrap, wrap };
|
|
934
|
+
//# sourceMappingURL=index.js.map
|
|
935
|
+
//# sourceMappingURL=index.js.map
|