@papyrus-sdk/ui-react-native 0.1.1 → 0.1.3
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/LICENSE +21 -0
- package/dist/index.js +578 -529
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +579 -529
- package/dist/index.mjs.map +1 -1
- package/package.json +44 -44
- package/runtime/index.html +541 -525
- package/runtime/runtime.js +440 -422
package/runtime/runtime.js
CHANGED
|
@@ -1,431 +1,449 @@
|
|
|
1
|
-
(function () {
|
|
2
|
-
const viewer = document.getElementById('viewer');
|
|
3
|
-
const DEFAULT_FONT_SIZE = 16;
|
|
4
|
-
const TEXT_PAGE_CHUNK = 1600;
|
|
5
|
-
|
|
6
|
-
let currentType = null;
|
|
7
|
-
let book = null;
|
|
8
|
-
let rendition = null;
|
|
9
|
-
let spineItems = [];
|
|
10
|
-
let textPages = [];
|
|
11
|
-
let textContainer = null;
|
|
12
|
-
let currentPage = 1;
|
|
13
|
-
let pageCount = 0;
|
|
14
|
-
let zoom = 1.0;
|
|
15
|
-
|
|
16
|
-
const sendMessage = (payload) => {
|
|
17
|
-
if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
|
|
18
|
-
window.ReactNativeWebView.postMessage(JSON.stringify(payload));
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
if (window.parent && window.parent !== window) {
|
|
22
|
-
window.parent.postMessage(payload, '*');
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const sendResponse = (id, ok, data, error) => {
|
|
27
|
-
sendMessage({ type: 'response', id, ok, data, error });
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const sendState = (extra) => {
|
|
31
|
-
sendMessage({
|
|
32
|
-
type: 'state',
|
|
33
|
-
payload: {
|
|
34
|
-
pageCount,
|
|
35
|
-
currentPage,
|
|
36
|
-
zoom,
|
|
37
|
-
...(extra || {}),
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const sendEvent = (name, payload) => {
|
|
43
|
-
sendMessage({ type: 'event', name, payload });
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const clearViewer = () => {
|
|
47
|
-
while (viewer.firstChild) {
|
|
48
|
-
viewer.removeChild(viewer.firstChild);
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const decodeBase64 = (value) => {
|
|
53
|
-
const clean = value.replace(/\s/g, '');
|
|
54
|
-
const binary = atob(clean);
|
|
55
|
-
const len = binary.length;
|
|
56
|
-
const bytes = new Uint8Array(len);
|
|
57
|
-
for (let i = 0; i < len; i += 1) {
|
|
58
|
-
bytes[i] = binary.charCodeAt(i);
|
|
59
|
-
}
|
|
60
|
-
return bytes;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const decodeBase64ToText = (value) => {
|
|
64
|
-
const bytes = decodeBase64(value);
|
|
65
|
-
return new TextDecoder('utf-8').decode(bytes);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const normalizeHref = (href) => {
|
|
69
|
-
if (!href) return '';
|
|
70
|
-
return href.split('#')[0];
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const getSpineIndexByHref = (href) => {
|
|
74
|
-
const normalized = normalizeHref(href);
|
|
75
|
-
if (!normalized) return -1;
|
|
76
|
-
return spineItems.findIndex((item) => normalizeHref(item.href) === normalized);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const buildOutline = async () => {
|
|
80
|
-
if (!book || !book.loaded || !book.loaded.navigation) return [];
|
|
81
|
-
const nav = await book.loaded.navigation;
|
|
82
|
-
const toc = nav && nav.toc ? nav.toc : [];
|
|
83
|
-
|
|
84
|
-
const mapItem = (item) => {
|
|
85
|
-
const title = item.label || item.title || '';
|
|
86
|
-
const pageIndex = getSpineIndexByHref(item.href || '');
|
|
87
|
-
const children = Array.isArray(item.subitems) ? item.subitems.map(mapItem) : [];
|
|
88
|
-
const outlineItem = { title, pageIndex };
|
|
89
|
-
if (children.length > 0) outlineItem.children = children;
|
|
90
|
-
return outlineItem;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
return toc.map(mapItem);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const applyEpubZoom = () => {
|
|
97
|
-
if (!rendition || !rendition.themes) return;
|
|
98
|
-
const fontSize = `${Math.round(zoom * 100)}%`;
|
|
99
|
-
if (typeof rendition.themes.fontSize === 'function') {
|
|
100
|
-
rendition.themes.fontSize(fontSize);
|
|
101
|
-
} else if (typeof rendition.themes.override === 'function') {
|
|
102
|
-
rendition.themes.override('font-size', fontSize);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const applyTextZoom = () => {
|
|
107
|
-
if (!textContainer) return;
|
|
108
|
-
const fontSize = Math.max(12, Math.round(DEFAULT_FONT_SIZE * zoom));
|
|
109
|
-
textContainer.style.fontSize = `${fontSize}px`;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const renderTextPage = (pageIndex) => {
|
|
113
|
-
const text = textPages[pageIndex] || '';
|
|
114
|
-
if (!textContainer) {
|
|
115
|
-
textContainer = document.createElement('div');
|
|
116
|
-
textContainer.style.padding = '24px';
|
|
117
|
-
textContainer.style.lineHeight = '1.6';
|
|
118
|
-
textContainer.style.whiteSpace = 'pre-wrap';
|
|
119
|
-
textContainer.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
|
120
|
-
viewer.appendChild(textContainer);
|
|
121
|
-
}
|
|
122
|
-
textContainer.textContent = text;
|
|
123
|
-
applyTextZoom();
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const paginateText = (text) => {
|
|
127
|
-
const pages = [];
|
|
128
|
-
for (let i = 0; i < text.length; i += TEXT_PAGE_CHUNK) {
|
|
129
|
-
pages.push(text.slice(i, i + TEXT_PAGE_CHUNK));
|
|
130
|
-
}
|
|
131
|
-
return pages.length > 0 ? pages : [''];
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const loadText = async (source) => {
|
|
135
|
-
let text = '';
|
|
136
|
-
if (source.kind === 'uri') {
|
|
137
|
-
const res = await fetch(source.uri);
|
|
138
|
-
text = await res.text();
|
|
139
|
-
} else if (source.kind === 'base64') {
|
|
140
|
-
text = decodeBase64ToText(source.data);
|
|
141
|
-
} else if (source.kind === 'text') {
|
|
142
|
-
text = source.text || '';
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
clearViewer();
|
|
146
|
-
textPages = paginateText(text);
|
|
147
|
-
pageCount = textPages.length;
|
|
148
|
-
currentPage = 1;
|
|
149
|
-
textContainer = null;
|
|
150
|
-
renderTextPage(0);
|
|
151
|
-
return { pageCount };
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const loadEpub = async (source) => {
|
|
155
|
-
if (rendition && typeof rendition.destroy === 'function') {
|
|
156
|
-
rendition.destroy();
|
|
157
|
-
}
|
|
158
|
-
if (book && typeof book.destroy === 'function') {
|
|
159
|
-
book.destroy();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
let data = null;
|
|
163
|
-
if (source.kind === 'uri') {
|
|
164
|
-
data = source.uri;
|
|
165
|
-
} else if (source.kind === 'base64') {
|
|
166
|
-
data = decodeBase64(source.data);
|
|
167
|
-
} else if (source.kind === 'text') {
|
|
168
|
-
const encoder = new TextEncoder();
|
|
169
|
-
data = encoder.encode(source.text || '').buffer;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
book = ePub(data);
|
|
173
|
-
await book.ready;
|
|
174
|
-
|
|
175
|
-
spineItems = book.spine && book.spine.items ? book.spine.items : [];
|
|
176
|
-
pageCount = spineItems.length;
|
|
177
|
-
currentPage = 1;
|
|
178
|
-
|
|
179
|
-
clearViewer();
|
|
180
|
-
rendition = book.renderTo(viewer, {
|
|
181
|
-
width: '100%',
|
|
182
|
-
height: '100%',
|
|
183
|
-
flow: 'paginated',
|
|
184
|
-
spread: 'none',
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
if (rendition && rendition.hooks && rendition.hooks.content) {
|
|
188
|
-
rendition.hooks.content.register((contents) => {
|
|
189
|
-
const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
|
|
190
|
-
if (frame) {
|
|
191
|
-
frame.setAttribute('sandbox', 'allow-scripts allow-same-origin');
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (rendition && typeof rendition.on === 'function') {
|
|
197
|
-
rendition.on('selected', (cfiRange, contents) => {
|
|
198
|
-
const selection = contents && contents.window ? contents.window.getSelection() : null;
|
|
199
|
-
const text = selection ? selection.toString().trim() : '';
|
|
200
|
-
if (text) {
|
|
201
|
-
sendEvent('TEXT_SELECTED', { text, pageIndex: Math.max(0, currentPage - 1) });
|
|
202
|
-
}
|
|
203
|
-
if (rendition && rendition.annotations && typeof rendition.annotations.remove === 'function') {
|
|
204
|
-
rendition.annotations.remove(cfiRange, 'highlight');
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
await displayEpubPage(0);
|
|
210
|
-
applyEpubZoom();
|
|
211
|
-
|
|
212
|
-
const outline = await buildOutline();
|
|
213
|
-
return { pageCount, outline };
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const displayEpubPage = async (pageIndex) => {
|
|
217
|
-
if (!rendition) return;
|
|
218
|
-
const item = spineItems[pageIndex];
|
|
219
|
-
if (!item) return;
|
|
220
|
-
const target = item.href || item.idref || item.cfiBase || pageIndex;
|
|
221
|
-
await rendition.display(target);
|
|
222
|
-
currentPage = pageIndex + 1;
|
|
223
|
-
sendState();
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const getTextContent = async (pageIndex) => {
|
|
227
|
-
if (currentType === 'text') {
|
|
228
|
-
const text = textPages[pageIndex] || '';
|
|
229
|
-
return [{
|
|
230
|
-
str: text,
|
|
231
|
-
dir: 'ltr',
|
|
232
|
-
width: 0,
|
|
233
|
-
height: 0,
|
|
234
|
-
transform: [1, 0, 0, 1, 0, 0],
|
|
235
|
-
fontName: 'default',
|
|
236
|
-
}];
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (currentType === 'epub') {
|
|
240
|
-
if (!book) return [];
|
|
241
|
-
const item = spineItems[pageIndex];
|
|
242
|
-
if (!item) return [];
|
|
243
|
-
try {
|
|
244
|
-
const section = book.spine.get(item.idref || item.href);
|
|
245
|
-
const text = section && typeof section.text === 'function' ? await section.text() : '';
|
|
246
|
-
if (!text) return [];
|
|
247
|
-
return [{
|
|
248
|
-
str: text,
|
|
249
|
-
dir: 'ltr',
|
|
250
|
-
width: 0,
|
|
251
|
-
height: 0,
|
|
252
|
-
transform: [1, 0, 0, 1, 0, 0],
|
|
253
|
-
fontName: 'default',
|
|
254
|
-
}];
|
|
255
|
-
} catch (err) {
|
|
256
|
-
return [];
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return [];
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const getPageText = async (pageIndex) => {
|
|
264
|
-
const items = await getTextContent(pageIndex);
|
|
265
|
-
return items.map((item) => item.str).join(' ');
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const searchText = async (query) => {
|
|
269
|
-
const normalized = query.toLowerCase();
|
|
270
|
-
const results = [];
|
|
271
|
-
|
|
272
|
-
for (let i = 0; i < pageCount; i += 1) {
|
|
273
|
-
const text = await getPageText(i);
|
|
274
|
-
if (!text) continue;
|
|
275
|
-
const lower = text.toLowerCase();
|
|
276
|
-
let pos = lower.indexOf(normalized, 0);
|
|
277
|
-
let matchIndex = 0;
|
|
278
|
-
while (pos !== -1) {
|
|
279
|
-
const start = Math.max(0, pos - 20);
|
|
280
|
-
const end = Math.min(text.length, pos + query.length + 20);
|
|
281
|
-
results.push({ pageIndex: i, text: text.substring(start, end), matchIndex });
|
|
282
|
-
matchIndex += 1;
|
|
283
|
-
pos = lower.indexOf(normalized, pos + 1);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return results;
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
const getPageDimensions = () => ({
|
|
291
|
-
width: viewer.clientWidth || 0,
|
|
292
|
-
height: viewer.clientHeight || 0,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
const getPageIndex = (dest) => {
|
|
296
|
-
if (
|
|
297
|
-
if (typeof dest === 'string') return getSpineIndexByHref(dest);
|
|
298
|
-
return null;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
1
|
+
(function () {
|
|
2
|
+
const viewer = document.getElementById('viewer');
|
|
3
|
+
const DEFAULT_FONT_SIZE = 16;
|
|
4
|
+
const TEXT_PAGE_CHUNK = 1600;
|
|
5
|
+
|
|
6
|
+
let currentType = null;
|
|
7
|
+
let book = null;
|
|
8
|
+
let rendition = null;
|
|
9
|
+
let spineItems = [];
|
|
10
|
+
let textPages = [];
|
|
11
|
+
let textContainer = null;
|
|
12
|
+
let currentPage = 1;
|
|
13
|
+
let pageCount = 0;
|
|
14
|
+
let zoom = 1.0;
|
|
15
|
+
|
|
16
|
+
const sendMessage = (payload) => {
|
|
17
|
+
if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
|
|
18
|
+
window.ReactNativeWebView.postMessage(JSON.stringify(payload));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (window.parent && window.parent !== window) {
|
|
22
|
+
window.parent.postMessage(payload, '*');
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const sendResponse = (id, ok, data, error) => {
|
|
27
|
+
sendMessage({ type: 'response', id, ok, data, error });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const sendState = (extra) => {
|
|
31
|
+
sendMessage({
|
|
32
|
+
type: 'state',
|
|
33
|
+
payload: {
|
|
34
|
+
pageCount,
|
|
35
|
+
currentPage,
|
|
36
|
+
zoom,
|
|
37
|
+
...(extra || {}),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const sendEvent = (name, payload) => {
|
|
43
|
+
sendMessage({ type: 'event', name, payload });
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const clearViewer = () => {
|
|
47
|
+
while (viewer.firstChild) {
|
|
48
|
+
viewer.removeChild(viewer.firstChild);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const decodeBase64 = (value) => {
|
|
53
|
+
const clean = value.replace(/\s/g, '');
|
|
54
|
+
const binary = atob(clean);
|
|
55
|
+
const len = binary.length;
|
|
56
|
+
const bytes = new Uint8Array(len);
|
|
57
|
+
for (let i = 0; i < len; i += 1) {
|
|
58
|
+
bytes[i] = binary.charCodeAt(i);
|
|
59
|
+
}
|
|
60
|
+
return bytes;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const decodeBase64ToText = (value) => {
|
|
64
|
+
const bytes = decodeBase64(value);
|
|
65
|
+
return new TextDecoder('utf-8').decode(bytes);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const normalizeHref = (href) => {
|
|
69
|
+
if (!href) return '';
|
|
70
|
+
return href.split('#')[0];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const getSpineIndexByHref = (href) => {
|
|
74
|
+
const normalized = normalizeHref(href);
|
|
75
|
+
if (!normalized) return -1;
|
|
76
|
+
return spineItems.findIndex((item) => normalizeHref(item.href) === normalized);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const buildOutline = async () => {
|
|
80
|
+
if (!book || !book.loaded || !book.loaded.navigation) return [];
|
|
81
|
+
const nav = await book.loaded.navigation;
|
|
82
|
+
const toc = nav && nav.toc ? nav.toc : [];
|
|
83
|
+
|
|
84
|
+
const mapItem = (item) => {
|
|
85
|
+
const title = item.label || item.title || '';
|
|
86
|
+
const pageIndex = getSpineIndexByHref(item.href || '');
|
|
87
|
+
const children = Array.isArray(item.subitems) ? item.subitems.map(mapItem) : [];
|
|
88
|
+
const outlineItem = { title, pageIndex };
|
|
89
|
+
if (children.length > 0) outlineItem.children = children;
|
|
90
|
+
return outlineItem;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return toc.map(mapItem);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const applyEpubZoom = () => {
|
|
97
|
+
if (!rendition || !rendition.themes) return;
|
|
98
|
+
const fontSize = `${Math.round(zoom * 100)}%`;
|
|
99
|
+
if (typeof rendition.themes.fontSize === 'function') {
|
|
100
|
+
rendition.themes.fontSize(fontSize);
|
|
101
|
+
} else if (typeof rendition.themes.override === 'function') {
|
|
102
|
+
rendition.themes.override('font-size', fontSize);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const applyTextZoom = () => {
|
|
107
|
+
if (!textContainer) return;
|
|
108
|
+
const fontSize = Math.max(12, Math.round(DEFAULT_FONT_SIZE * zoom));
|
|
109
|
+
textContainer.style.fontSize = `${fontSize}px`;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const renderTextPage = (pageIndex) => {
|
|
113
|
+
const text = textPages[pageIndex] || '';
|
|
114
|
+
if (!textContainer) {
|
|
115
|
+
textContainer = document.createElement('div');
|
|
116
|
+
textContainer.style.padding = '24px';
|
|
117
|
+
textContainer.style.lineHeight = '1.6';
|
|
118
|
+
textContainer.style.whiteSpace = 'pre-wrap';
|
|
119
|
+
textContainer.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
|
120
|
+
viewer.appendChild(textContainer);
|
|
121
|
+
}
|
|
122
|
+
textContainer.textContent = text;
|
|
123
|
+
applyTextZoom();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const paginateText = (text) => {
|
|
127
|
+
const pages = [];
|
|
128
|
+
for (let i = 0; i < text.length; i += TEXT_PAGE_CHUNK) {
|
|
129
|
+
pages.push(text.slice(i, i + TEXT_PAGE_CHUNK));
|
|
130
|
+
}
|
|
131
|
+
return pages.length > 0 ? pages : [''];
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const loadText = async (source) => {
|
|
135
|
+
let text = '';
|
|
136
|
+
if (source.kind === 'uri') {
|
|
137
|
+
const res = await fetch(source.uri);
|
|
138
|
+
text = await res.text();
|
|
139
|
+
} else if (source.kind === 'base64') {
|
|
140
|
+
text = decodeBase64ToText(source.data);
|
|
141
|
+
} else if (source.kind === 'text') {
|
|
142
|
+
text = source.text || '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
clearViewer();
|
|
146
|
+
textPages = paginateText(text);
|
|
147
|
+
pageCount = textPages.length;
|
|
148
|
+
currentPage = 1;
|
|
149
|
+
textContainer = null;
|
|
150
|
+
renderTextPage(0);
|
|
151
|
+
return { pageCount };
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const loadEpub = async (source) => {
|
|
155
|
+
if (rendition && typeof rendition.destroy === 'function') {
|
|
156
|
+
rendition.destroy();
|
|
157
|
+
}
|
|
158
|
+
if (book && typeof book.destroy === 'function') {
|
|
159
|
+
book.destroy();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let data = null;
|
|
163
|
+
if (source.kind === 'uri') {
|
|
164
|
+
data = source.uri;
|
|
165
|
+
} else if (source.kind === 'base64') {
|
|
166
|
+
data = decodeBase64(source.data);
|
|
167
|
+
} else if (source.kind === 'text') {
|
|
168
|
+
const encoder = new TextEncoder();
|
|
169
|
+
data = encoder.encode(source.text || '').buffer;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
book = ePub(data);
|
|
173
|
+
await book.ready;
|
|
174
|
+
|
|
175
|
+
spineItems = book.spine && book.spine.items ? book.spine.items : [];
|
|
176
|
+
pageCount = spineItems.length;
|
|
177
|
+
currentPage = 1;
|
|
178
|
+
|
|
179
|
+
clearViewer();
|
|
180
|
+
rendition = book.renderTo(viewer, {
|
|
181
|
+
width: '100%',
|
|
182
|
+
height: '100%',
|
|
183
|
+
flow: 'paginated',
|
|
184
|
+
spread: 'none',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (rendition && rendition.hooks && rendition.hooks.content) {
|
|
188
|
+
rendition.hooks.content.register((contents) => {
|
|
189
|
+
const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
|
|
190
|
+
if (frame) {
|
|
191
|
+
frame.setAttribute('sandbox', 'allow-scripts allow-same-origin');
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (rendition && typeof rendition.on === 'function') {
|
|
197
|
+
rendition.on('selected', (cfiRange, contents) => {
|
|
198
|
+
const selection = contents && contents.window ? contents.window.getSelection() : null;
|
|
199
|
+
const text = selection ? selection.toString().trim() : '';
|
|
200
|
+
if (text) {
|
|
201
|
+
sendEvent('TEXT_SELECTED', { text, pageIndex: Math.max(0, currentPage - 1) });
|
|
202
|
+
}
|
|
203
|
+
if (rendition && rendition.annotations && typeof rendition.annotations.remove === 'function') {
|
|
204
|
+
rendition.annotations.remove(cfiRange, 'highlight');
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
await displayEpubPage(0);
|
|
210
|
+
applyEpubZoom();
|
|
211
|
+
|
|
212
|
+
const outline = await buildOutline();
|
|
213
|
+
return { pageCount, outline };
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const displayEpubPage = async (pageIndex) => {
|
|
217
|
+
if (!rendition) return;
|
|
218
|
+
const item = spineItems[pageIndex];
|
|
219
|
+
if (!item) return;
|
|
220
|
+
const target = item.href || item.idref || item.cfiBase || pageIndex;
|
|
221
|
+
await rendition.display(target);
|
|
222
|
+
currentPage = pageIndex + 1;
|
|
223
|
+
sendState();
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const getTextContent = async (pageIndex) => {
|
|
227
|
+
if (currentType === 'text') {
|
|
228
|
+
const text = textPages[pageIndex] || '';
|
|
229
|
+
return [{
|
|
230
|
+
str: text,
|
|
231
|
+
dir: 'ltr',
|
|
232
|
+
width: 0,
|
|
233
|
+
height: 0,
|
|
234
|
+
transform: [1, 0, 0, 1, 0, 0],
|
|
235
|
+
fontName: 'default',
|
|
236
|
+
}];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (currentType === 'epub') {
|
|
240
|
+
if (!book) return [];
|
|
241
|
+
const item = spineItems[pageIndex];
|
|
242
|
+
if (!item) return [];
|
|
243
|
+
try {
|
|
244
|
+
const section = book.spine.get(item.idref || item.href);
|
|
245
|
+
const text = section && typeof section.text === 'function' ? await section.text() : '';
|
|
246
|
+
if (!text) return [];
|
|
247
|
+
return [{
|
|
248
|
+
str: text,
|
|
249
|
+
dir: 'ltr',
|
|
250
|
+
width: 0,
|
|
251
|
+
height: 0,
|
|
252
|
+
transform: [1, 0, 0, 1, 0, 0],
|
|
253
|
+
fontName: 'default',
|
|
254
|
+
}];
|
|
255
|
+
} catch (err) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return [];
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const getPageText = async (pageIndex) => {
|
|
264
|
+
const items = await getTextContent(pageIndex);
|
|
265
|
+
return items.map((item) => item.str).join(' ');
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const searchText = async (query) => {
|
|
269
|
+
const normalized = query.toLowerCase();
|
|
270
|
+
const results = [];
|
|
271
|
+
|
|
272
|
+
for (let i = 0; i < pageCount; i += 1) {
|
|
273
|
+
const text = await getPageText(i);
|
|
274
|
+
if (!text) continue;
|
|
275
|
+
const lower = text.toLowerCase();
|
|
276
|
+
let pos = lower.indexOf(normalized, 0);
|
|
277
|
+
let matchIndex = 0;
|
|
278
|
+
while (pos !== -1) {
|
|
279
|
+
const start = Math.max(0, pos - 20);
|
|
280
|
+
const end = Math.min(text.length, pos + query.length + 20);
|
|
281
|
+
results.push({ pageIndex: i, text: text.substring(start, end), matchIndex });
|
|
282
|
+
matchIndex += 1;
|
|
283
|
+
pos = lower.indexOf(normalized, pos + 1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return results;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const getPageDimensions = () => ({
|
|
291
|
+
width: viewer.clientWidth || 0,
|
|
292
|
+
height: viewer.clientHeight || 0,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const getPageIndex = (dest) => {
|
|
296
|
+
if (!dest) return null;
|
|
297
|
+
if (typeof dest === 'string') return getSpineIndexByHref(dest);
|
|
298
|
+
if (typeof dest !== 'object') return null;
|
|
299
|
+
if (dest.kind === 'href') return getSpineIndexByHref(dest.value);
|
|
300
|
+
if (dest.kind === 'pageIndex') return dest.value;
|
|
301
|
+
if (dest.kind === 'pageNumber') return Math.max(0, dest.value - 1);
|
|
302
|
+
return null;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const selectText = async (pageIndex) => {
|
|
306
|
+
const text = await getPageText(pageIndex);
|
|
307
|
+
if (!text) return null;
|
|
308
|
+
return {
|
|
309
|
+
text,
|
|
310
|
+
rects: [{ x: 0, y: 0, width: 1, height: 1 }],
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const applyZoom = () => {
|
|
315
|
+
if (currentType === 'text') {
|
|
316
|
+
applyTextZoom();
|
|
317
|
+
} else if (currentType === 'epub') {
|
|
318
|
+
applyEpubZoom();
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const handleCommand = async (message) => {
|
|
323
|
+
const { id, kind, payload } = message;
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
if (kind === 'load') {
|
|
327
|
+
currentType = payload.type;
|
|
328
|
+
zoom = 1.0;
|
|
329
|
+
|
|
330
|
+
if (currentType === 'text') {
|
|
331
|
+
const result = await loadText(payload.source);
|
|
332
|
+
sendState();
|
|
333
|
+
sendResponse(id, true, result);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (currentType === 'epub') {
|
|
338
|
+
const result = await loadEpub(payload.source);
|
|
339
|
+
sendState({ outline: result.outline || [] });
|
|
340
|
+
sendResponse(id, true, result);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
throw new Error('Unsupported document type');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (kind === 'go-to-page') {
|
|
348
|
+
const page = Math.max(1, payload.page || 1);
|
|
349
|
+
if (currentType === 'text') {
|
|
350
|
+
currentPage = page;
|
|
351
|
+
renderTextPage(page - 1);
|
|
352
|
+
sendState();
|
|
353
|
+
} else if (currentType === 'epub') {
|
|
354
|
+
await displayEpubPage(page - 1);
|
|
355
|
+
}
|
|
356
|
+
sendResponse(id, true, { currentPage });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (kind === 'set-zoom') {
|
|
361
|
+
zoom = Math.max(0.5, Math.min(4.0, payload.zoom || 1.0));
|
|
362
|
+
applyZoom();
|
|
363
|
+
sendState();
|
|
364
|
+
sendResponse(id, true, { zoom });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (kind === 'set-rotation') {
|
|
369
|
+
sendResponse(id, true, {});
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (kind === 'get-text-content') {
|
|
374
|
+
const items = await getTextContent(payload.pageIndex || 0);
|
|
375
|
+
sendResponse(id, true, items);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (kind === 'get-page-dimensions') {
|
|
380
|
+
sendResponse(id, true, getPageDimensions());
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (kind === 'search-text') {
|
|
385
|
+
const results = await searchText(payload.query || '');
|
|
386
|
+
sendResponse(id, true, results);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (kind === 'select-text') {
|
|
391
|
+
const selection = await selectText(payload.pageIndex || 0);
|
|
392
|
+
sendResponse(id, true, selection);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (kind === 'get-outline') {
|
|
397
|
+
const outline = await buildOutline();
|
|
398
|
+
sendResponse(id, true, outline);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (kind === 'get-page-index') {
|
|
403
|
+
const index = getPageIndex(payload.dest);
|
|
404
|
+
sendResponse(id, true, index);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (kind === 'destroy') {
|
|
409
|
+
clearViewer();
|
|
410
|
+
sendResponse(id, true, {});
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
sendResponse(id, false, null, 'Unknown command');
|
|
415
|
+
} catch (err) {
|
|
416
|
+
sendResponse(id, false, null, err && err.message ? err.message : String(err));
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const onMessage = (event) => {
|
|
421
|
+
let message = null;
|
|
422
|
+
const raw = event && typeof event === 'object' ? event.data : null;
|
|
423
|
+
|
|
424
|
+
if (raw && typeof raw === 'object' && raw.kind && raw.id) {
|
|
425
|
+
message = raw;
|
|
426
|
+
} else if (typeof raw === 'string') {
|
|
427
|
+
if (raw.startsWith('setImmediate$')) return;
|
|
428
|
+
const trimmed = raw.trim();
|
|
429
|
+
if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) {
|
|
401
430
|
return;
|
|
402
431
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
sendResponse(id, true, {});
|
|
432
|
+
try {
|
|
433
|
+
message = JSON.parse(trimmed);
|
|
434
|
+
} catch (err) {
|
|
407
435
|
return;
|
|
408
436
|
}
|
|
409
|
-
|
|
410
|
-
sendResponse(id, false, null, 'Unknown command');
|
|
411
|
-
} catch (err) {
|
|
412
|
-
sendResponse(id, false, null, err && err.message ? err.message : String(err));
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
const onMessage = (event) => {
|
|
417
|
-
let message = null;
|
|
418
|
-
try {
|
|
419
|
-
message = JSON.parse(event.data);
|
|
420
|
-
} catch (err) {
|
|
437
|
+
} else {
|
|
421
438
|
return;
|
|
422
439
|
}
|
|
423
|
-
|
|
440
|
+
|
|
441
|
+
if (!message || !message.kind || message.id == null) return;
|
|
424
442
|
handleCommand(message);
|
|
425
443
|
};
|
|
426
|
-
|
|
427
|
-
window.addEventListener('message', onMessage);
|
|
428
|
-
document.addEventListener('message', onMessage);
|
|
429
|
-
|
|
430
|
-
sendMessage({ type: 'ready' });
|
|
444
|
+
|
|
445
|
+
window.addEventListener('message', onMessage);
|
|
446
|
+
document.addEventListener('message', onMessage);
|
|
447
|
+
|
|
448
|
+
sendMessage({ type: 'ready' });
|
|
431
449
|
})();
|