@caipira/vue-reader 0.0.1

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.
Files changed (64) hide show
  1. package/README.md +153 -0
  2. package/dist/composables/useEpubReaderStrategy.d.ts +19 -0
  3. package/dist/composables/useEpubReaderStrategy.js +1 -0
  4. package/dist/composables/useEpubReaderWasm.d.ts +2 -0
  5. package/dist/composables/useEpubReaderWasm.js +131 -0
  6. package/dist/composables/useEpubReaderZipFetcher.d.ts +2 -0
  7. package/dist/composables/useEpubReaderZipFetcher.js +29 -0
  8. package/dist/services/browser/epub.d.ts +4 -0
  9. package/dist/services/browser/epub.js +22 -0
  10. package/dist/services/browser/manifest.d.ts +7 -0
  11. package/dist/services/browser/manifest.js +224 -0
  12. package/dist/services/browser/url-rewrite.d.ts +5 -0
  13. package/dist/services/browser/url-rewrite.js +183 -0
  14. package/dist/services/browser/url.d.ts +11 -0
  15. package/dist/services/browser/url.js +82 -0
  16. package/dist/services/browser/zip-fetcher.d.ts +12 -0
  17. package/dist/services/browser/zip-fetcher.js +123 -0
  18. package/dist/services/common/progression.d.ts +32 -0
  19. package/dist/services/common/progression.js +169 -0
  20. package/dist/services/common/title.d.ts +11 -0
  21. package/dist/services/common/title.js +40 -0
  22. package/dist/services/common/word-decorations.d.ts +4 -0
  23. package/dist/services/common/word-decorations.js +316 -0
  24. package/dist/services/common/word-lookup.d.ts +4 -0
  25. package/dist/services/common/word-lookup.js +154 -0
  26. package/dist/services/wasm/frame-document-bridge.d.ts +1 -0
  27. package/dist/services/wasm/frame-document-bridge.js +84 -0
  28. package/dist/services/wasm/wasm-streamer.d.ts +35 -0
  29. package/dist/services/wasm/wasm-streamer.js +157 -0
  30. package/dist/src/composables/useEpubReader.d.ts +27788 -0
  31. package/dist/src/composables/useEpubReader.js +8 -0
  32. package/dist/src/composables/useEpubReaderController.d.ts +27787 -0
  33. package/dist/src/composables/useEpubReaderController.js +23 -0
  34. package/dist/src/composables/useEpubReaderNavigation.d.ts +6 -0
  35. package/dist/src/composables/useEpubReaderNavigation.js +26 -0
  36. package/dist/src/composables/useEpubReaderSettings.d.ts +15 -0
  37. package/dist/src/composables/useEpubReaderSettings.js +8 -0
  38. package/dist/src/composables/useEpubReaderState.d.ts +22 -0
  39. package/dist/src/composables/useEpubReaderState.js +29 -0
  40. package/dist/src/composables/useReaderDictionary.d.ts +8 -0
  41. package/dist/src/composables/useReaderDictionary.js +13 -0
  42. package/dist/src/composables/useReaderSettings.d.ts +25 -0
  43. package/dist/src/composables/useReaderSettings.js +42 -0
  44. package/dist/src/composables/utils.d.ts +2 -0
  45. package/dist/src/composables/utils.js +1 -0
  46. package/dist/src/core/controller.d.ts +27789 -0
  47. package/dist/src/core/controller.js +262 -0
  48. package/dist/src/core/fonts.d.ts +1 -0
  49. package/dist/src/core/fonts.js +55 -0
  50. package/dist/src/core/settings-store.d.ts +16 -0
  51. package/dist/src/core/settings-store.js +58 -0
  52. package/dist/src/core/storage.d.ts +2 -0
  53. package/dist/src/core/storage.js +38 -0
  54. package/dist/src/index.d.ts +13 -0
  55. package/dist/src/index.js +11 -0
  56. package/dist/src/settings/options.d.ts +20 -0
  57. package/dist/src/settings/options.js +27 -0
  58. package/dist/src/types.d.ts +40 -0
  59. package/dist/src/types.js +1 -0
  60. package/dist/types/browser.d.ts +1 -0
  61. package/dist/types/browser.js +1 -0
  62. package/dist/types/common.d.ts +55 -0
  63. package/dist/types/common.js +1 -0
  64. package/package.json +46 -0
@@ -0,0 +1,316 @@
1
+ import { toRaw } from 'vue';
2
+ import { debounce } from 'lodash';
3
+ import { getReaderColors } from '../../src/composables/useReaderSettings';
4
+ import { useReaderDictionary } from '../../src/composables/useReaderDictionary';
5
+ const GROUP_KNOWN = 'known-words-known';
6
+ const GROUP_UNKNOWN = 'known-words-unknown';
7
+ const MAX_VISIBLE_WORDS = 2000;
8
+ const CONTEXT_WINDOW = 18;
9
+ const REFRESH_DELAY_MS = 300;
10
+ const WORD_REGEX = /[a-zA-Z0-9\u00C0-\u00FF]+(?:[’']?[a-zA-Z\u00C0-\u00FF]+)?/g;
11
+ const HAS_ALPHA_REGEX = /[a-zA-Z\u00C0-\u00FF]/;
12
+ const intersectsViewport = (wnd, rect) => {
13
+ return (rect.bottom > 0 &&
14
+ rect.right > 0 &&
15
+ rect.top < wnd.innerHeight &&
16
+ rect.left < wnd.innerWidth);
17
+ };
18
+ const normalizeWord = (raw) => {
19
+ return raw
20
+ .trim()
21
+ .replace(/’/g, "'")
22
+ .toLowerCase()
23
+ .replace(/^[^a-z0-9\u00C0-\u00FF']+|[^a-z0-9\u00C0-\u00FF']+$/gi, '');
24
+ };
25
+ const isHighlightableNode = (parent) => {
26
+ if (!parent)
27
+ return false;
28
+ const tag = parent.tagName.toLowerCase();
29
+ if (['script', 'style', 'noscript', 'svg', 'math'].includes(tag)) {
30
+ return false;
31
+ }
32
+ const text = parent.textContent?.trim() ?? '';
33
+ return text.length > 0;
34
+ };
35
+ const selectorForElement = (wnd, element) => {
36
+ const generator = wnd._readium_cssSelectorGenerator;
37
+ if (generator?.getCssSelector) {
38
+ try {
39
+ return generator.getCssSelector(element, {
40
+ selectors: ['tag', 'id', 'class', 'nthchild', 'nthoftype', 'attribute'],
41
+ });
42
+ }
43
+ catch { }
44
+ }
45
+ if (element.id) {
46
+ return `#${CSS.escape(element.id)}`;
47
+ }
48
+ const parent = element.parentElement;
49
+ if (!parent) {
50
+ return undefined;
51
+ }
52
+ const idx = Array.from(parent.children).indexOf(element) + 1;
53
+ return `${parent.tagName.toLowerCase()} > ${element.tagName.toLowerCase()}:nth-child(${idx})`;
54
+ };
55
+ const collectVisibleCandidates = (wnd, chapterHref) => {
56
+ const doc = wnd.document;
57
+ const root = doc?.body;
58
+ if (!root) {
59
+ return [];
60
+ }
61
+ const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT);
62
+ const out = [];
63
+ let textNode = walker.nextNode();
64
+ let seen = 0;
65
+ while (textNode && out.length < MAX_VISIBLE_WORDS) {
66
+ const parent = textNode.parentElement;
67
+ if (isHighlightableNode(parent)) {
68
+ const text = textNode.nodeValue ?? '';
69
+ if (text.trim().length > 0) {
70
+ const range = doc.createRange();
71
+ range.selectNodeContents(textNode);
72
+ const visible = Array.from(range.getClientRects()).some((rect) => intersectsViewport(wnd, rect));
73
+ if (visible) {
74
+ const selector = selectorForElement(wnd, parent);
75
+ if (selector) {
76
+ WORD_REGEX.lastIndex = 0;
77
+ let match;
78
+ while ((match = WORD_REGEX.exec(text)) &&
79
+ out.length < MAX_VISIBLE_WORDS) {
80
+ const highlight = match[0] ?? '';
81
+ // Skips tokens without at least one alpha char
82
+ if (!HAS_ALPHA_REGEX.test(highlight)) {
83
+ continue;
84
+ }
85
+ const lemma = normalizeWord(highlight);
86
+ if (!lemma || lemma.length <= 1) {
87
+ continue;
88
+ }
89
+ const start = match.index;
90
+ const end = start + highlight.length;
91
+ const beforeRaw = text.slice(Math.max(0, start - CONTEXT_WINDOW), start);
92
+ const afterRaw = text.slice(end, Math.min(text.length, end + CONTEXT_WINDOW));
93
+ const before = beforeRaw.trimStart();
94
+ const after = afterRaw.trimEnd();
95
+ const id = `${chapterHref}|${selector}|${start}|${highlight.toLowerCase()}`;
96
+ out.push({
97
+ id,
98
+ lemma,
99
+ locator: {
100
+ href: chapterHref,
101
+ type: 'application/xhtml+xml',
102
+ locations: { cssSelector: selector },
103
+ text: {
104
+ before: before.length > 0 ? before : undefined,
105
+ highlight,
106
+ after: after.length > 0 ? after : undefined,
107
+ },
108
+ },
109
+ });
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ textNode = walker.nextNode();
116
+ seen += 1;
117
+ if (seen > 5000) {
118
+ break;
119
+ }
120
+ }
121
+ return out;
122
+ };
123
+ export const createReadiumWordDecorationsController = (navigatorRef, languageRef) => {
124
+ const dictionary = useReaderDictionary();
125
+ const activeDecorations = new Map();
126
+ let refreshSeq = 0;
127
+ let currentWindow = null;
128
+ let currentChapterHref = '';
129
+ let destroyed = false;
130
+ const scheduleRefresh = debounce(() => {
131
+ void refresh();
132
+ }, REFRESH_DELAY_MS);
133
+ const scheduleScrollRefresh = debounce(() => {
134
+ void refresh();
135
+ }, REFRESH_DELAY_MS);
136
+ const postDecorationCommand = (command) => {
137
+ const nav = toRaw(navigatorRef.value);
138
+ const frame = nav?._cframes?.find((f) => f?.iframe?.contentWindow === currentWindow) ??
139
+ nav?._cframes?.[0];
140
+ const comms = frame?.msg;
141
+ if (!comms || typeof comms.send !== 'function') {
142
+ return;
143
+ }
144
+ comms.send('decorate', command);
145
+ };
146
+ const cleanupFrameListeners = (() => {
147
+ let offScroll = null;
148
+ let offResize = null;
149
+ return {
150
+ set(wnd) {
151
+ this.clear();
152
+ const onScroll = () => {
153
+ scheduleScrollRefresh();
154
+ };
155
+ const onResize = () => {
156
+ scheduleRefresh();
157
+ };
158
+ wnd.addEventListener('scroll', onScroll, { passive: true });
159
+ wnd.addEventListener('resize', onResize, { passive: true });
160
+ offScroll = () => wnd.removeEventListener('scroll', onScroll);
161
+ offResize = () => wnd.removeEventListener('resize', onResize);
162
+ },
163
+ clear() {
164
+ offScroll?.();
165
+ offResize?.();
166
+ offScroll = null;
167
+ offResize = null;
168
+ scheduleScrollRefresh.cancel();
169
+ },
170
+ };
171
+ })();
172
+ const refresh = async () => {
173
+ if (destroyed) {
174
+ return;
175
+ }
176
+ refreshSeq += 1;
177
+ const wnd = currentWindow;
178
+ const chapterHref = currentChapterHref;
179
+ const language = languageRef();
180
+ if (!wnd || !chapterHref || !language) {
181
+ return;
182
+ }
183
+ const candidates = collectVisibleCandidates(wnd, chapterHref);
184
+ const lemmas = Array.from(new Set(candidates.map((c) => c.lemma)));
185
+ const statuses = await dictionary.classifyWords(language, lemmas, `${chapterHref}|${language}`);
186
+ const next = new Map();
187
+ let knownCount = 0;
188
+ let unknownCount = 0;
189
+ for (const candidate of candidates) {
190
+ const status = statuses.get(candidate.lemma) ?? 'unknown';
191
+ if (status === 'known') {
192
+ knownCount += 1;
193
+ }
194
+ else {
195
+ unknownCount += 1;
196
+ }
197
+ const tint = status === 'unknown'
198
+ ? (getReaderColors().highlight ?? undefined)
199
+ : undefined;
200
+ const nextGroup = status === 'unknown' ? GROUP_UNKNOWN : GROUP_KNOWN;
201
+ const decorated = {
202
+ ...candidate,
203
+ group: nextGroup,
204
+ status,
205
+ tint,
206
+ };
207
+ next.set(candidate.id, decorated);
208
+ const previous = activeDecorations.get(candidate.id);
209
+ if (!previous) {
210
+ if (status === 'known') {
211
+ continue;
212
+ }
213
+ postDecorationCommand({
214
+ group: nextGroup,
215
+ action: 'add',
216
+ decoration: {
217
+ id: candidate.id,
218
+ locator: decorated.locator,
219
+ style: {
220
+ tint,
221
+ layout: 'boxes',
222
+ },
223
+ },
224
+ });
225
+ continue;
226
+ }
227
+ const previousGroup = previous.group ?? GROUP_KNOWN;
228
+ if (previousGroup !== nextGroup) {
229
+ if (previousGroup === GROUP_UNKNOWN) {
230
+ postDecorationCommand({
231
+ group: previousGroup,
232
+ action: 'remove',
233
+ decoration: { id: candidate.id },
234
+ });
235
+ }
236
+ if (nextGroup === GROUP_UNKNOWN) {
237
+ postDecorationCommand({
238
+ group: nextGroup,
239
+ action: 'add',
240
+ decoration: {
241
+ id: candidate.id,
242
+ locator: decorated.locator,
243
+ style: {
244
+ tint,
245
+ layout: 'boxes',
246
+ },
247
+ },
248
+ });
249
+ }
250
+ continue;
251
+ }
252
+ if (nextGroup === GROUP_UNKNOWN &&
253
+ (previous.tint !== tint || previous.status !== status)) {
254
+ postDecorationCommand({
255
+ group: nextGroup,
256
+ action: 'update',
257
+ decoration: {
258
+ id: candidate.id,
259
+ locator: decorated.locator,
260
+ style: {
261
+ tint,
262
+ layout: 'boxes',
263
+ },
264
+ },
265
+ });
266
+ }
267
+ }
268
+ for (const [id, value] of activeDecorations) {
269
+ if (!next.has(id)) {
270
+ postDecorationCommand({
271
+ group: value.group ?? GROUP_KNOWN,
272
+ action: 'remove',
273
+ decoration: { id },
274
+ });
275
+ }
276
+ }
277
+ activeDecorations.clear();
278
+ for (const [id, value] of next) {
279
+ activeDecorations.set(id, value);
280
+ }
281
+ };
282
+ const onFrameLoaded = (wnd, chapterHref) => {
283
+ scheduleRefresh.cancel();
284
+ scheduleScrollRefresh.cancel();
285
+ currentWindow = wnd;
286
+ currentChapterHref = chapterHref;
287
+ cleanupFrameListeners.set(wnd);
288
+ scheduleRefresh();
289
+ };
290
+ const onPositionChanged = (chapterHref) => {
291
+ if (chapterHref === currentChapterHref) {
292
+ return;
293
+ }
294
+ currentChapterHref = chapterHref;
295
+ scheduleRefresh();
296
+ };
297
+ const destroy = () => {
298
+ destroyed = true;
299
+ scheduleRefresh.cancel();
300
+ scheduleScrollRefresh.cancel();
301
+ cleanupFrameListeners.clear();
302
+ if (activeDecorations.size > 0) {
303
+ postDecorationCommand({ group: GROUP_UNKNOWN, action: 'clear' });
304
+ }
305
+ activeDecorations.clear();
306
+ dictionary.clear();
307
+ currentWindow = null;
308
+ currentChapterHref = '';
309
+ };
310
+ return {
311
+ onFrameLoaded,
312
+ onPositionChanged,
313
+ refresh,
314
+ destroy,
315
+ };
316
+ };
@@ -0,0 +1,4 @@
1
+ export declare const createReadiumWordLookupController: (languageRef: () => string | undefined) => {
2
+ onFrameLoaded: (wnd: Window) => void;
3
+ destroy: () => void;
4
+ };
@@ -0,0 +1,154 @@
1
+ import { useReaderDictionary } from '../../src/composables/useReaderDictionary';
2
+ const EPUB_WORD_REGEX = /[a-zA-Z0-9\u00C0-\u00FF]+(?:[’']?[a-zA-Z\u00C0-\u00FF]+)?/g;
3
+ const normalizeLookupText = (text) => {
4
+ return text.replace(/\s+/g, ' ').trim();
5
+ };
6
+ const buildContextFromText = (text, start, end) => {
7
+ const raw = normalizeLookupText(text);
8
+ if (!raw) {
9
+ return '';
10
+ }
11
+ const safeStart = Math.max(0, Math.min(start, raw.length));
12
+ const safeEnd = Math.max(safeStart, Math.min(end, raw.length));
13
+ const left = Math.max(0, safeStart - 80);
14
+ const right = Math.min(raw.length, safeEnd + 80);
15
+ return raw.slice(left, right).trim();
16
+ };
17
+ const getTextHitFromPointer = (wnd, event) => {
18
+ const doc = wnd.document;
19
+ if (typeof doc.caretRangeFromPoint === 'function') {
20
+ const range = doc.caretRangeFromPoint(event.clientX, event.clientY);
21
+ if (range?.startContainer?.nodeType === Node.TEXT_NODE) {
22
+ return {
23
+ node: range.startContainer,
24
+ offset: range.startOffset,
25
+ };
26
+ }
27
+ }
28
+ if (typeof doc.caretPositionFromPoint === 'function') {
29
+ const caret = doc.caretPositionFromPoint(event.clientX, event.clientY);
30
+ if (caret?.offsetNode?.nodeType === Node.TEXT_NODE) {
31
+ return {
32
+ node: caret.offsetNode,
33
+ offset: caret.offset,
34
+ };
35
+ }
36
+ }
37
+ return null;
38
+ };
39
+ const getLookupPayloadFromEvent = (wnd, event) => {
40
+ const target = event.target;
41
+ if (target?.closest('a, button, input, textarea, select, [contenteditable="true"]')) {
42
+ return null;
43
+ }
44
+ const textHit = getTextHitFromPointer(wnd, event);
45
+ if (!textHit) {
46
+ return null;
47
+ }
48
+ const text = textHit.node.nodeValue ?? '';
49
+ if (!text) {
50
+ return null;
51
+ }
52
+ EPUB_WORD_REGEX.lastIndex = 0;
53
+ let match;
54
+ while ((match = EPUB_WORD_REGEX.exec(text))) {
55
+ const word = match[0] ?? '';
56
+ const start = match.index;
57
+ const end = start + word.length;
58
+ if (textHit.offset >= start && textHit.offset <= end) {
59
+ const context = buildContextFromText(text, start, end);
60
+ return {
61
+ word,
62
+ context,
63
+ };
64
+ }
65
+ }
66
+ return null;
67
+ };
68
+ export const createReadiumWordLookupController = (languageRef) => {
69
+ const dictionary = useReaderDictionary();
70
+ let cleanupLookupListener = null;
71
+ const onFrameLoaded = (wnd) => {
72
+ cleanupLookupListener?.();
73
+ cleanupLookupListener = null;
74
+ let lookupCursorVisible = false;
75
+ let lastPointerEvent = null;
76
+ const setLookupCursor = (visible) => {
77
+ if (lookupCursorVisible === visible) {
78
+ return;
79
+ }
80
+ lookupCursorVisible = visible;
81
+ wnd.document.body.style.cursor = visible ? 'pointer' : '';
82
+ };
83
+ const refreshLookupCursor = (event) => {
84
+ if (event) {
85
+ lastPointerEvent = event;
86
+ }
87
+ const pointerEvent = event ?? lastPointerEvent;
88
+ if (!pointerEvent || !(pointerEvent.ctrlKey || pointerEvent.metaKey)) {
89
+ setLookupCursor(false);
90
+ return;
91
+ }
92
+ const payload = getLookupPayloadFromEvent(wnd, pointerEvent);
93
+ setLookupCursor(Boolean(payload));
94
+ };
95
+ const onPointerClick = (event) => {
96
+ if (!(event.ctrlKey || event.metaKey)) {
97
+ return;
98
+ }
99
+ const language = languageRef();
100
+ if (!language) {
101
+ return;
102
+ }
103
+ const payload = getLookupPayloadFromEvent(wnd, event);
104
+ if (!payload) {
105
+ return;
106
+ }
107
+ event.preventDefault();
108
+ event.stopPropagation();
109
+ dictionary.onWordLookup?.({
110
+ language,
111
+ word: payload.word,
112
+ context: payload.context,
113
+ });
114
+ };
115
+ const onPointerMove = (event) => {
116
+ refreshLookupCursor(event);
117
+ };
118
+ const onModifierKeyDown = () => {
119
+ refreshLookupCursor();
120
+ };
121
+ const onModifierKeyUp = () => {
122
+ refreshLookupCursor();
123
+ };
124
+ const onPointerLeave = () => {
125
+ setLookupCursor(false);
126
+ };
127
+ const onWindowBlur = () => {
128
+ setLookupCursor(false);
129
+ };
130
+ wnd.document.addEventListener('click', onPointerClick, true);
131
+ wnd.document.addEventListener('mousemove', onPointerMove, true);
132
+ wnd.document.addEventListener('keydown', onModifierKeyDown, true);
133
+ wnd.document.addEventListener('keyup', onModifierKeyUp, true);
134
+ wnd.document.addEventListener('mouseleave', onPointerLeave, true);
135
+ wnd.addEventListener('blur', onWindowBlur);
136
+ cleanupLookupListener = () => {
137
+ wnd.document.removeEventListener('click', onPointerClick, true);
138
+ wnd.document.removeEventListener('mousemove', onPointerMove, true);
139
+ wnd.document.removeEventListener('keydown', onModifierKeyDown, true);
140
+ wnd.document.removeEventListener('keyup', onModifierKeyUp, true);
141
+ wnd.document.removeEventListener('mouseleave', onPointerLeave, true);
142
+ wnd.removeEventListener('blur', onWindowBlur);
143
+ wnd.document.body.style.cursor = '';
144
+ };
145
+ };
146
+ const destroy = () => {
147
+ cleanupLookupListener?.();
148
+ cleanupLookupListener = null;
149
+ };
150
+ return {
151
+ onFrameLoaded,
152
+ destroy,
153
+ };
154
+ };
@@ -0,0 +1 @@
1
+ export declare const installFrameDocumentBridge: () => (() => void);
@@ -0,0 +1,84 @@
1
+ const FRAME_DOC_CACHE = 'epub-frame-doc-v1';
2
+ const FRAME_DOC_PREFIX = '/epub-streamer/frame-doc/';
3
+ let uninstallBridge = null;
4
+ let bridgeRefCount = 0;
5
+ const isHtmlLikeBlob = (obj) => {
6
+ if (!(obj instanceof Blob))
7
+ return false;
8
+ const type = (obj.type || '').toLowerCase();
9
+ return (type.includes('text/html') ||
10
+ type.includes('application/xhtml+xml') ||
11
+ type.includes('+xml'));
12
+ };
13
+ const cacheFrameDocument = async (url, blob) => {
14
+ const cache = await caches.open(FRAME_DOC_CACHE);
15
+ await cache.put(new Request(url), new Response(blob, {
16
+ headers: {
17
+ 'Content-Type': blob.type || 'application/xhtml+xml',
18
+ 'Cache-Control': 'no-store',
19
+ },
20
+ }));
21
+ };
22
+ const deleteFrameDocument = async (url) => {
23
+ const cache = await caches.open(FRAME_DOC_CACHE);
24
+ await cache.delete(new Request(url));
25
+ };
26
+ const createFrameDocumentUrl = () => {
27
+ const id = crypto.randomUUID();
28
+ return `${window.location.origin}${FRAME_DOC_PREFIX}${id}`;
29
+ };
30
+ export const installFrameDocumentBridge = () => {
31
+ bridgeRefCount += 1;
32
+ if (uninstallBridge)
33
+ return uninstallBridge;
34
+ const nativeCreate = URL.createObjectURL.bind(URL);
35
+ const nativeRevoke = URL.revokeObjectURL.bind(URL);
36
+ const syntheticToNative = new Map();
37
+ URL.createObjectURL = ((obj) => {
38
+ if (!isHtmlLikeBlob(obj)) {
39
+ return nativeCreate(obj);
40
+ }
41
+ const syntheticUrl = createFrameDocumentUrl();
42
+ const nativeUrl = nativeCreate(obj);
43
+ syntheticToNative.set(syntheticUrl, nativeUrl);
44
+ void cacheFrameDocument(syntheticUrl, obj).catch((err) => {
45
+ console.warn('[frame-bridge] failed to cache frame doc', {
46
+ syntheticUrl,
47
+ err,
48
+ });
49
+ });
50
+ return syntheticUrl;
51
+ });
52
+ URL.revokeObjectURL = ((url) => {
53
+ const nativeUrl = syntheticToNative.get(url);
54
+ if (nativeUrl) {
55
+ syntheticToNative.delete(url);
56
+ nativeRevoke(nativeUrl);
57
+ void deleteFrameDocument(url).catch((err) => {
58
+ console.warn('[frame-bridge] failed to delete cached frame doc', {
59
+ url,
60
+ err,
61
+ });
62
+ });
63
+ return;
64
+ }
65
+ nativeRevoke(url);
66
+ });
67
+ uninstallBridge = () => {
68
+ bridgeRefCount = Math.max(0, bridgeRefCount - 1);
69
+ if (bridgeRefCount > 0)
70
+ return;
71
+ URL.createObjectURL = nativeCreate;
72
+ URL.revokeObjectURL = nativeRevoke;
73
+ const urls = Array.from(syntheticToNative.keys());
74
+ urls.forEach((syntheticUrl) => {
75
+ const nativeUrl = syntheticToNative.get(syntheticUrl);
76
+ if (nativeUrl)
77
+ nativeRevoke(nativeUrl);
78
+ void deleteFrameDocument(syntheticUrl);
79
+ });
80
+ syntheticToNative.clear();
81
+ uninstallBridge = null;
82
+ };
83
+ return uninstallBridge;
84
+ };
@@ -0,0 +1,35 @@
1
+ import type { Publication as RPublicationType } from '@readium/shared';
2
+ type WasmStreamerConfig = {
3
+ swScope: string;
4
+ swUrl: string;
5
+ wasmUrl: string;
6
+ };
7
+ declare const setWasmStreamerConfig: (next: Partial<WasmStreamerConfig>) => void;
8
+ /**
9
+ * Ensure the Service Worker is registered.
10
+ *
11
+ * Register the root-level SW script so it can control the whole page.
12
+ * The passthrough callback in sw.js ensures only requests under
13
+ * /epub-streamer/webpub/ reach the Go handler; everything else goes to the
14
+ * network normally.
15
+ */
16
+ declare function ensureSW(): Promise<ServiceWorkerRegistration>;
17
+ /**
18
+ * Tell the Service Worker / WASM module to load a publication from a URL.
19
+ * The WASM module fetches the EPUB from the given URL, parses it, and
20
+ * makes it available via the streamer endpoints.
21
+ */
22
+ declare function loadPublication(bookId: string, epubUrl: string): Promise<void>;
23
+ /**
24
+ * Create a Readium Publication backed by the WASM streamer.
25
+ *
26
+ * The HttpFetcher resolves resource requests against the base URL,
27
+ * which points into the SW scope. The SW intercepts these and
28
+ * the Go WASM handler serves them.
29
+ */
30
+ declare function createPublication(bookId: string): Promise<RPublicationType>;
31
+ /**
32
+ * Unload a publication from the WASM streamer (cleanup).
33
+ */
34
+ declare function unloadPublication(bookId: string): Promise<void>;
35
+ export { ensureSW, loadPublication, createPublication, unloadPublication, setWasmStreamerConfig, };