@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.
- package/README.md +153 -0
- package/dist/composables/useEpubReaderStrategy.d.ts +19 -0
- package/dist/composables/useEpubReaderStrategy.js +1 -0
- package/dist/composables/useEpubReaderWasm.d.ts +2 -0
- package/dist/composables/useEpubReaderWasm.js +131 -0
- package/dist/composables/useEpubReaderZipFetcher.d.ts +2 -0
- package/dist/composables/useEpubReaderZipFetcher.js +29 -0
- package/dist/services/browser/epub.d.ts +4 -0
- package/dist/services/browser/epub.js +22 -0
- package/dist/services/browser/manifest.d.ts +7 -0
- package/dist/services/browser/manifest.js +224 -0
- package/dist/services/browser/url-rewrite.d.ts +5 -0
- package/dist/services/browser/url-rewrite.js +183 -0
- package/dist/services/browser/url.d.ts +11 -0
- package/dist/services/browser/url.js +82 -0
- package/dist/services/browser/zip-fetcher.d.ts +12 -0
- package/dist/services/browser/zip-fetcher.js +123 -0
- package/dist/services/common/progression.d.ts +32 -0
- package/dist/services/common/progression.js +169 -0
- package/dist/services/common/title.d.ts +11 -0
- package/dist/services/common/title.js +40 -0
- package/dist/services/common/word-decorations.d.ts +4 -0
- package/dist/services/common/word-decorations.js +316 -0
- package/dist/services/common/word-lookup.d.ts +4 -0
- package/dist/services/common/word-lookup.js +154 -0
- package/dist/services/wasm/frame-document-bridge.d.ts +1 -0
- package/dist/services/wasm/frame-document-bridge.js +84 -0
- package/dist/services/wasm/wasm-streamer.d.ts +35 -0
- package/dist/services/wasm/wasm-streamer.js +157 -0
- package/dist/src/composables/useEpubReader.d.ts +27788 -0
- package/dist/src/composables/useEpubReader.js +8 -0
- package/dist/src/composables/useEpubReaderController.d.ts +27787 -0
- package/dist/src/composables/useEpubReaderController.js +23 -0
- package/dist/src/composables/useEpubReaderNavigation.d.ts +6 -0
- package/dist/src/composables/useEpubReaderNavigation.js +26 -0
- package/dist/src/composables/useEpubReaderSettings.d.ts +15 -0
- package/dist/src/composables/useEpubReaderSettings.js +8 -0
- package/dist/src/composables/useEpubReaderState.d.ts +22 -0
- package/dist/src/composables/useEpubReaderState.js +29 -0
- package/dist/src/composables/useReaderDictionary.d.ts +8 -0
- package/dist/src/composables/useReaderDictionary.js +13 -0
- package/dist/src/composables/useReaderSettings.d.ts +25 -0
- package/dist/src/composables/useReaderSettings.js +42 -0
- package/dist/src/composables/utils.d.ts +2 -0
- package/dist/src/composables/utils.js +1 -0
- package/dist/src/core/controller.d.ts +27789 -0
- package/dist/src/core/controller.js +262 -0
- package/dist/src/core/fonts.d.ts +1 -0
- package/dist/src/core/fonts.js +55 -0
- package/dist/src/core/settings-store.d.ts +16 -0
- package/dist/src/core/settings-store.js +58 -0
- package/dist/src/core/storage.d.ts +2 -0
- package/dist/src/core/storage.js +38 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/index.js +11 -0
- package/dist/src/settings/options.d.ts +20 -0
- package/dist/src/settings/options.js +27 -0
- package/dist/src/types.d.ts +40 -0
- package/dist/src/types.js +1 -0
- package/dist/types/browser.d.ts +1 -0
- package/dist/types/browser.js +1 -0
- package/dist/types/common.d.ts +55 -0
- package/dist/types/common.js +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { getExt, resolveHref, parseSrcset, guessMediaType, } from '../../services/browser/url';
|
|
2
|
+
const blobUrlCache = new Map();
|
|
3
|
+
const getBlobUrl = (zip, path) => {
|
|
4
|
+
const existing = blobUrlCache.get(path);
|
|
5
|
+
if (existing) {
|
|
6
|
+
return existing;
|
|
7
|
+
}
|
|
8
|
+
const data = zip[path];
|
|
9
|
+
if (!data) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const mime = guessMediaType(getExt(path));
|
|
13
|
+
// Ensure data is ArrayBuffer or valid BlobPart
|
|
14
|
+
let blobPart;
|
|
15
|
+
if (data instanceof Uint8Array) {
|
|
16
|
+
blobPart =
|
|
17
|
+
data.buffer instanceof ArrayBuffer
|
|
18
|
+
? data.buffer
|
|
19
|
+
: new Uint8Array(data).buffer;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
blobPart = data;
|
|
23
|
+
}
|
|
24
|
+
const blob = new Blob([blobPart], { type: mime });
|
|
25
|
+
const url = URL.createObjectURL(blob);
|
|
26
|
+
blobUrlCache.set(path, url);
|
|
27
|
+
return url;
|
|
28
|
+
};
|
|
29
|
+
const clearBlobUrlCache = () => {
|
|
30
|
+
blobUrlCache.forEach((url) => {
|
|
31
|
+
URL.revokeObjectURL(url);
|
|
32
|
+
});
|
|
33
|
+
blobUrlCache.clear();
|
|
34
|
+
};
|
|
35
|
+
const rewriteXhtmlUrls = (doc, resourceHref, zip) => {
|
|
36
|
+
const ATTRIBUTE_TARGETS = [
|
|
37
|
+
['img', 'src'],
|
|
38
|
+
['source', 'src'],
|
|
39
|
+
['source', 'srcset'],
|
|
40
|
+
['video', 'src'],
|
|
41
|
+
['video', 'poster'],
|
|
42
|
+
['audio', 'src'],
|
|
43
|
+
['script', 'src'],
|
|
44
|
+
['link', 'href'],
|
|
45
|
+
['object', 'data'],
|
|
46
|
+
['embed', 'src'],
|
|
47
|
+
['image', 'href'],
|
|
48
|
+
['use', 'href'],
|
|
49
|
+
];
|
|
50
|
+
// SVG xlink:href
|
|
51
|
+
try {
|
|
52
|
+
const useEls = doc.querySelectorAll('use');
|
|
53
|
+
for (const el of Array.from(useEls)) {
|
|
54
|
+
const val = el.getAttribute('xlink:href') ||
|
|
55
|
+
el.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
|
|
56
|
+
if (!val) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(val)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const resolved = resolveHref(resourceHref, val);
|
|
63
|
+
const blobUrl = getBlobUrl(zip, resolved);
|
|
64
|
+
if (blobUrl) {
|
|
65
|
+
el.setAttributeNS('http://www.w3.org/1999/xlink', 'href', blobUrl);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// ignore
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const imgEls = doc.querySelectorAll('image');
|
|
74
|
+
for (const el of Array.from(imgEls)) {
|
|
75
|
+
const val = el.getAttribute('xlink:href') ||
|
|
76
|
+
el.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
|
|
77
|
+
if (!val) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(val)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const resolved = resolveHref(resourceHref, val);
|
|
84
|
+
const blobUrl = getBlobUrl(zip, resolved);
|
|
85
|
+
if (blobUrl) {
|
|
86
|
+
el.setAttributeNS('http://www.w3.org/1999/xlink', 'href', blobUrl);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// ignore
|
|
92
|
+
}
|
|
93
|
+
for (const [tag, attr] of ATTRIBUTE_TARGETS) {
|
|
94
|
+
const selector = `${tag}[${attr}]`;
|
|
95
|
+
let elements;
|
|
96
|
+
try {
|
|
97
|
+
elements = Array.from(doc.querySelectorAll(selector));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
for (const el of elements) {
|
|
103
|
+
const val = el.getAttribute(attr);
|
|
104
|
+
if (!val) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (attr === 'srcset') {
|
|
108
|
+
const urls = parseSrcset(val);
|
|
109
|
+
let changed = false;
|
|
110
|
+
const parts = val.split(',').map((part) => {
|
|
111
|
+
const trimmed = part.trim();
|
|
112
|
+
const firstSpace = trimmed.indexOf(' ');
|
|
113
|
+
const src = firstSpace < 0
|
|
114
|
+
? trimmed
|
|
115
|
+
: trimmed.substring(0, firstSpace).trim();
|
|
116
|
+
const rest = firstSpace >= 0 ? trimmed.substring(firstSpace) : '';
|
|
117
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(src)) {
|
|
118
|
+
return part;
|
|
119
|
+
}
|
|
120
|
+
const resolved = resolveHref(resourceHref, src);
|
|
121
|
+
const blobUrl = getBlobUrl(zip, resolved);
|
|
122
|
+
if (blobUrl) {
|
|
123
|
+
changed = true;
|
|
124
|
+
return blobUrl + rest;
|
|
125
|
+
}
|
|
126
|
+
return part;
|
|
127
|
+
});
|
|
128
|
+
if (changed) {
|
|
129
|
+
el.setAttribute(attr, parts.join(', '));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(val)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const resolved = resolveHref(resourceHref, val);
|
|
137
|
+
const blobUrl = getBlobUrl(zip, resolved);
|
|
138
|
+
if (blobUrl) {
|
|
139
|
+
el.setAttribute(attr, blobUrl);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Handle url() references in style attributes and <style> elements
|
|
145
|
+
styleUrlRewriteLoop(doc, resourceHref, zip);
|
|
146
|
+
};
|
|
147
|
+
const styleUrlRewriteLoop = (doc, resourceHref, zip) => {
|
|
148
|
+
const URL_RE = /url\(\s*["']?([^"')\\s]+)["']?\s*\)/g;
|
|
149
|
+
const rewriteFn = (_match, url) => {
|
|
150
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
|
|
151
|
+
return _match;
|
|
152
|
+
}
|
|
153
|
+
const resolved = resolveHref(resourceHref, url);
|
|
154
|
+
const blobUrl = getBlobUrl(zip, resolved);
|
|
155
|
+
return blobUrl ? `url(${blobUrl})` : _match;
|
|
156
|
+
};
|
|
157
|
+
for (const el of Array.from(doc.querySelectorAll('[style]'))) {
|
|
158
|
+
const style = el.getAttribute('style');
|
|
159
|
+
if (style && URL_RE.test(style)) {
|
|
160
|
+
URL_RE.lastIndex = 0;
|
|
161
|
+
el.setAttribute('style', style.replace(URL_RE, rewriteFn));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const el of Array.from(doc.querySelectorAll('style'))) {
|
|
165
|
+
const content = el.textContent;
|
|
166
|
+
if (content && URL_RE.test(content)) {
|
|
167
|
+
URL_RE.lastIndex = 0;
|
|
168
|
+
el.textContent = content.replace(URL_RE, rewriteFn);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const rewriteCssUrls = (css, resourceHref, zip) => {
|
|
173
|
+
const URL_RE = /url\(\s*["']?([^"')\\s]+)["']?\s*\)/g;
|
|
174
|
+
return css.replace(URL_RE, (_match, url) => {
|
|
175
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
|
|
176
|
+
return _match;
|
|
177
|
+
}
|
|
178
|
+
const resolved = resolveHref(resourceHref, url);
|
|
179
|
+
const blobUrl = getBlobUrl(zip, resolved);
|
|
180
|
+
return blobUrl ? `url(${blobUrl})` : _match;
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
export { clearBlobUrlCache, rewriteXhtmlUrls, rewriteCssUrls };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare const guessMediaType: (ext: string) => string;
|
|
2
|
+
declare const getExt: (path: string) => string;
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a relative URL against a base resource path (absolute in the zip).
|
|
5
|
+
*
|
|
6
|
+
* e.g. resolveHref('OEBPS/Text/ch1.xhtml', '../Images/title.jpg')
|
|
7
|
+
* → 'OEBPS/Images/title.jpg'
|
|
8
|
+
*/
|
|
9
|
+
declare const resolveHref: (base: string, relative: string) => string;
|
|
10
|
+
declare const parseSrcset: (value: string) => string[];
|
|
11
|
+
export { guessMediaType, getExt, resolveHref, parseSrcset };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const guessMediaType = (ext) => {
|
|
2
|
+
const m = {
|
|
3
|
+
xhtml: 'application/xhtml+xml',
|
|
4
|
+
html: 'text/html',
|
|
5
|
+
htm: 'text/html',
|
|
6
|
+
css: 'text/css',
|
|
7
|
+
js: 'text/javascript',
|
|
8
|
+
ncx: 'application/x-dtbncx+xml',
|
|
9
|
+
xml: 'application/xml',
|
|
10
|
+
smil: 'application/smil+xml',
|
|
11
|
+
png: 'image/png',
|
|
12
|
+
jpg: 'image/jpeg',
|
|
13
|
+
jpeg: 'image/jpeg',
|
|
14
|
+
gif: 'image/gif',
|
|
15
|
+
svg: 'image/svg+xml',
|
|
16
|
+
webp: 'image/webp',
|
|
17
|
+
avif: 'image/avif',
|
|
18
|
+
otf: 'font/otf',
|
|
19
|
+
ttf: 'font/ttf',
|
|
20
|
+
woff: 'font/woff',
|
|
21
|
+
woff2: 'font/woff2',
|
|
22
|
+
mp3: 'audio/mpeg',
|
|
23
|
+
mp4: 'audio/mp4',
|
|
24
|
+
ogg: 'audio/ogg',
|
|
25
|
+
wav: 'audio/wav',
|
|
26
|
+
webm: 'video/webm',
|
|
27
|
+
opf: 'application/oebps-package+xml',
|
|
28
|
+
pls: 'application/pls+xml',
|
|
29
|
+
};
|
|
30
|
+
return m[ext] ?? 'application/octet-stream';
|
|
31
|
+
};
|
|
32
|
+
const getExt = (path) => {
|
|
33
|
+
const i = path.lastIndexOf('.');
|
|
34
|
+
return i >= 0 ? path.slice(i + 1).toLowerCase() : '';
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a relative URL against a base resource path (absolute in the zip).
|
|
38
|
+
*
|
|
39
|
+
* e.g. resolveHref('OEBPS/Text/ch1.xhtml', '../Images/title.jpg')
|
|
40
|
+
* → 'OEBPS/Images/title.jpg'
|
|
41
|
+
*/
|
|
42
|
+
const resolveHref = (base, relative) => {
|
|
43
|
+
const rel = relative.trim();
|
|
44
|
+
if (rel.startsWith('/')) {
|
|
45
|
+
return rel.slice(1);
|
|
46
|
+
}
|
|
47
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(rel)) {
|
|
48
|
+
return rel;
|
|
49
|
+
}
|
|
50
|
+
const dir = base.lastIndexOf('/') >= 0 ? base.substring(0, base.lastIndexOf('/') + 1) : '';
|
|
51
|
+
const stack = (dir + rel).split('/');
|
|
52
|
+
const result = [];
|
|
53
|
+
for (const seg of stack) {
|
|
54
|
+
if (seg === '.' || seg === '') {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (seg === '..') {
|
|
58
|
+
if (result.length > 0) {
|
|
59
|
+
result.pop();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
result.push(seg);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result.join('/');
|
|
67
|
+
};
|
|
68
|
+
const parseSrcset = (value) => {
|
|
69
|
+
const urls = [];
|
|
70
|
+
for (const part of value.split(',')) {
|
|
71
|
+
const trimmed = part.trim();
|
|
72
|
+
const firstSpace = trimmed.indexOf(' ');
|
|
73
|
+
if (firstSpace < 0) {
|
|
74
|
+
urls.push(trimmed);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
urls.push(trimmed.substring(0, firstSpace).trim());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return urls;
|
|
81
|
+
};
|
|
82
|
+
export { guessMediaType, getExt, resolveHref, parseSrcset };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Fetcher } from '@readium/shared';
|
|
2
|
+
import type { ZipData } from '../../types/browser';
|
|
3
|
+
import { Link } from '@readium/shared';
|
|
4
|
+
import { Resource } from '@readium/shared';
|
|
5
|
+
declare class ZipFetcher implements Fetcher {
|
|
6
|
+
private zip;
|
|
7
|
+
constructor(zip: ZipData);
|
|
8
|
+
links(): Link[];
|
|
9
|
+
get(link: Link): Resource;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|
|
12
|
+
export default ZipFetcher;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Link } from '@readium/shared';
|
|
2
|
+
import { Resource } from '@readium/shared';
|
|
3
|
+
import { guessMediaType, getExt } from '../../services/browser/url';
|
|
4
|
+
import { rewriteCssUrls, rewriteXhtmlUrls, clearBlobUrlCache, } from '../../services/browser/url-rewrite';
|
|
5
|
+
const isHtmlType = (path) => {
|
|
6
|
+
const ext = getExt(path);
|
|
7
|
+
return ext === 'xhtml' || ext === 'html' || ext === 'htm' || ext === 'svg';
|
|
8
|
+
};
|
|
9
|
+
class ZipResource extends Resource {
|
|
10
|
+
zip;
|
|
11
|
+
_link;
|
|
12
|
+
constructor(zip, link) {
|
|
13
|
+
super();
|
|
14
|
+
this.zip = zip;
|
|
15
|
+
this._link = link;
|
|
16
|
+
}
|
|
17
|
+
async link() {
|
|
18
|
+
return this._link;
|
|
19
|
+
}
|
|
20
|
+
async length() {
|
|
21
|
+
return this.zip[this._link.href]?.length;
|
|
22
|
+
}
|
|
23
|
+
async read(range) {
|
|
24
|
+
const data = this.zip[this._link.href];
|
|
25
|
+
if (!data) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (range) {
|
|
29
|
+
return data.slice(range.start, range.endInclusive + 1);
|
|
30
|
+
}
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
async readAsString() {
|
|
34
|
+
const data = this.zip[this._link.href];
|
|
35
|
+
if (!data) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
let str = new TextDecoder('utf-8').decode(data).replace(/^\uFEFF/, '');
|
|
39
|
+
// Replace HTML-only named entities invalid in XHTML
|
|
40
|
+
str = str.replace(/&(?!(?:amp|lt|gt|quot|apos);)([a-zA-Z][a-zA-Z0-9]*);/g, (match) => {
|
|
41
|
+
const el = document.createElement('span');
|
|
42
|
+
el.innerHTML = match;
|
|
43
|
+
return el.textContent ?? match;
|
|
44
|
+
});
|
|
45
|
+
// Rewrite relative resource URLs for HTML/XHTML/SVG and CSS
|
|
46
|
+
if (isHtmlType(this._link.href)) {
|
|
47
|
+
const mime = this._link.type &&
|
|
48
|
+
(this._link.type === 'application/xhtml+xml' ||
|
|
49
|
+
this._link.type === 'text/html')
|
|
50
|
+
? this._link.type
|
|
51
|
+
: guessMediaType(getExt(this._link.href));
|
|
52
|
+
try {
|
|
53
|
+
const doc = new DOMParser().parseFromString(str, mime);
|
|
54
|
+
const err = doc.querySelector('parsererror');
|
|
55
|
+
if (!err) {
|
|
56
|
+
rewriteXhtmlUrls(doc, this._link.href, this.zip);
|
|
57
|
+
// Readium's go_id handler uses document.getElementById(id),
|
|
58
|
+
// which does not match name attributes. Copy name→id so
|
|
59
|
+
// anchor fragments resolve correctly.
|
|
60
|
+
for (const el of Array.from(doc.querySelectorAll('[name]'))) {
|
|
61
|
+
if (!el.id && el.getAttribute('name')) {
|
|
62
|
+
el.id = el.getAttribute('name');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (this._link.type === 'application/xhtml+xml' ||
|
|
66
|
+
mime === 'application/xhtml+xml' ||
|
|
67
|
+
getExt(this._link.href) === 'xhtml' ||
|
|
68
|
+
getExt(this._link.href) === 'svg') {
|
|
69
|
+
str = new XMLSerializer().serializeToString(doc);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
str = doc.documentElement.outerHTML;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// If parsing fails, continue with the original string
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (getExt(this._link.href) === 'css') {
|
|
81
|
+
try {
|
|
82
|
+
str = rewriteCssUrls(str, this._link.href, this.zip);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// If rewriting fails, continue with the original string
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Ensure <html> has the XHTML namespace
|
|
89
|
+
if (/<html[^>]*>/i.test(str) &&
|
|
90
|
+
!/xmlns\s*=\s*["']http:\/\/www\.w3\.org\/1999\/xhtml["']/i.test(str)) {
|
|
91
|
+
str = str.replace(/(<html)([^>]*>)/i, (_m, open, rest) => `${open} xmlns="http://www.w3.org/1999/xhtml"${rest}`);
|
|
92
|
+
}
|
|
93
|
+
return str;
|
|
94
|
+
}
|
|
95
|
+
async readAsJSON() {
|
|
96
|
+
const str = await this.readAsString();
|
|
97
|
+
return str ? JSON.parse(str) : undefined;
|
|
98
|
+
}
|
|
99
|
+
async readAsXML() {
|
|
100
|
+
const str = await this.readAsString();
|
|
101
|
+
if (!str) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return new DOMParser().parseFromString(str, 'application/xml');
|
|
105
|
+
}
|
|
106
|
+
close() { }
|
|
107
|
+
}
|
|
108
|
+
class ZipFetcher {
|
|
109
|
+
zip;
|
|
110
|
+
constructor(zip) {
|
|
111
|
+
this.zip = zip;
|
|
112
|
+
}
|
|
113
|
+
links() {
|
|
114
|
+
return Object.keys(this.zip).map((path) => new Link({ href: path, type: guessMediaType(getExt(path)) }));
|
|
115
|
+
}
|
|
116
|
+
get(link) {
|
|
117
|
+
return new ZipResource(this.zip, link);
|
|
118
|
+
}
|
|
119
|
+
close() {
|
|
120
|
+
clearBlobUrlCache();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export default ZipFetcher;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ZipData } from '../../types/browser';
|
|
2
|
+
import type { ChapterBoundary } from '../../types/common';
|
|
3
|
+
import { Link, Locator, Publication } from '@readium/shared';
|
|
4
|
+
/**
|
|
5
|
+
* Scan the content files referenced by TOC entries and compute
|
|
6
|
+
* approximate progression boundaries for each chapter anchor.
|
|
7
|
+
*
|
|
8
|
+
* This is needed for books where all chapters live in a single spine
|
|
9
|
+
* file (e.g. EPUB 2 with fragment anchors) – the spine link always
|
|
10
|
+
* reports the same href, so we use progression within the file to
|
|
11
|
+
* determine which chapter the reader is on.
|
|
12
|
+
*/
|
|
13
|
+
declare const computeChapterProgressions: (toc: Link[], zip: ZipData) => Map<string, ChapterBoundary[]>;
|
|
14
|
+
declare const generatePositions: (pub: Publication) => Locator[];
|
|
15
|
+
declare const buildWeightsFromZip: (pub: Publication, zip: ZipData) => ((idx: number, rawProg?: number) => {
|
|
16
|
+
progress: number;
|
|
17
|
+
resourceProgression: number;
|
|
18
|
+
});
|
|
19
|
+
declare const buildWeightsFromResolver: (pub: Publication, resolveSize: (href: string) => number | undefined) => ((idx: number, rawProg?: number) => {
|
|
20
|
+
progress: number;
|
|
21
|
+
resourceProgression: number;
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Save the current position (spine index and progression) for the given URL in sessionStorage.
|
|
25
|
+
*/
|
|
26
|
+
declare const savePosition: (url: string, persisted: Locator) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Restore the saved position for the given URL from sessionStorage.
|
|
29
|
+
*/
|
|
30
|
+
declare const restorePosition: (url: string, publication: Publication) => Locator | undefined;
|
|
31
|
+
declare const computePublicationChaptersSize: (publication: Publication) => Promise<Map<string, number>>;
|
|
32
|
+
export { savePosition, restorePosition, generatePositions, buildWeightsFromZip, buildWeightsFromResolver, computeChapterProgressions, computePublicationChaptersSize, };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Locator, LocatorLocations } from '@readium/shared';
|
|
2
|
+
/**
|
|
3
|
+
* Scan the content files referenced by TOC entries and compute
|
|
4
|
+
* approximate progression boundaries for each chapter anchor.
|
|
5
|
+
*
|
|
6
|
+
* This is needed for books where all chapters live in a single spine
|
|
7
|
+
* file (e.g. EPUB 2 with fragment anchors) – the spine link always
|
|
8
|
+
* reports the same href, so we use progression within the file to
|
|
9
|
+
* determine which chapter the reader is on.
|
|
10
|
+
*/
|
|
11
|
+
const computeChapterProgressions = (toc, zip) => {
|
|
12
|
+
const result = new Map();
|
|
13
|
+
for (const tocLink of toc) {
|
|
14
|
+
if (!tocLink.title)
|
|
15
|
+
continue;
|
|
16
|
+
const parts = tocLink.href.split('#');
|
|
17
|
+
const baseHref = parts[0];
|
|
18
|
+
const fragment = parts[1];
|
|
19
|
+
if (!baseHref || !fragment)
|
|
20
|
+
continue;
|
|
21
|
+
// Read the raw text so we can find the anchor position by offset
|
|
22
|
+
const raw = zip[baseHref];
|
|
23
|
+
if (!raw)
|
|
24
|
+
continue;
|
|
25
|
+
let content;
|
|
26
|
+
try {
|
|
27
|
+
content = new TextDecoder('utf-8').decode(raw);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
// Escape special regex chars in the fragment ID
|
|
33
|
+
const escaped = fragment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
34
|
+
// Look for id="fragment" or name="fragment"
|
|
35
|
+
const pattern = new RegExp(`(?:id|name)\\s*=\\s*["']${escaped}["']`);
|
|
36
|
+
const match = content.match(pattern);
|
|
37
|
+
if (!match || match.index === undefined) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const offset = match.index;
|
|
41
|
+
const prog = content.length > 0 ? offset / content.length : 0;
|
|
42
|
+
let entries = result.get(baseHref);
|
|
43
|
+
if (!entries) {
|
|
44
|
+
entries = [];
|
|
45
|
+
result.set(baseHref, entries);
|
|
46
|
+
}
|
|
47
|
+
entries.push({ title: tocLink.title, startProg: prog });
|
|
48
|
+
}
|
|
49
|
+
// Sort each file's entries by progression ascending
|
|
50
|
+
for (const [, entries] of result) {
|
|
51
|
+
entries.sort((a, b) => a.startProg - b.startProg);
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
const generatePositions = (pub) => {
|
|
56
|
+
const items = pub.readingOrder.items;
|
|
57
|
+
if (items.length === 0) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return items.map((link, index) => {
|
|
61
|
+
return new Locator({
|
|
62
|
+
href: link.href,
|
|
63
|
+
type: link.type || 'application/xhtml+xml',
|
|
64
|
+
title: link.title,
|
|
65
|
+
locations: new LocatorLocations({
|
|
66
|
+
position: index + 1,
|
|
67
|
+
progression: 0,
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Returns a function that computes overall publication progression based on
|
|
74
|
+
* the current spine index and progression within that spine item, using
|
|
75
|
+
* weights derived from the sizes of the spine items.
|
|
76
|
+
*/
|
|
77
|
+
const buildWeightedProgressCalculator = (resourceWeights) => {
|
|
78
|
+
const totalWeight = resourceWeights.reduce((sum, rw) => sum + rw.size, 0);
|
|
79
|
+
let cumulativeWeights = [];
|
|
80
|
+
let resourceProgression = 0;
|
|
81
|
+
let currentResourceIdx = 0;
|
|
82
|
+
let cum = 0;
|
|
83
|
+
cumulativeWeights = resourceWeights.map((rw) => {
|
|
84
|
+
const before = cum;
|
|
85
|
+
cum += rw.size;
|
|
86
|
+
return before;
|
|
87
|
+
});
|
|
88
|
+
return (idx, rawProg) => {
|
|
89
|
+
let progress = 0;
|
|
90
|
+
if (idx !== currentResourceIdx) {
|
|
91
|
+
currentResourceIdx = idx;
|
|
92
|
+
if (rawProg !== undefined && rawProg >= 0 && rawProg <= 1) {
|
|
93
|
+
resourceProgression = rawProg;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
resourceProgression = 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (rawProg !== undefined && rawProg >= 0 && rawProg <= 1) {
|
|
100
|
+
resourceProgression = rawProg;
|
|
101
|
+
}
|
|
102
|
+
if (totalWeight > 0 && idx < resourceWeights.length) {
|
|
103
|
+
const w = resourceWeights[idx].size;
|
|
104
|
+
const before = cumulativeWeights[idx];
|
|
105
|
+
progress = (before + resourceProgression * w) / totalWeight;
|
|
106
|
+
}
|
|
107
|
+
return { progress, resourceProgression };
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
const buildWeightsFromZip = (pub, zip) => {
|
|
111
|
+
const resourceWeights = pub.readingOrder.items.map((item) => ({
|
|
112
|
+
href: item.href,
|
|
113
|
+
size: zip[item.href]?.length ?? 1,
|
|
114
|
+
}));
|
|
115
|
+
return buildWeightedProgressCalculator(resourceWeights);
|
|
116
|
+
};
|
|
117
|
+
const buildWeightsFromResolver = (pub, resolveSize) => {
|
|
118
|
+
const resourceWeights = pub.readingOrder.items.map((item) => ({
|
|
119
|
+
href: item.href,
|
|
120
|
+
size: resolveSize(item.href) ?? 1,
|
|
121
|
+
}));
|
|
122
|
+
return buildWeightedProgressCalculator(resourceWeights);
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Save the current position (spine index and progression) for the given URL in sessionStorage.
|
|
126
|
+
*/
|
|
127
|
+
const savePosition = (url, persisted) => {
|
|
128
|
+
sessionStorage.setItem(`epub-locator:${url}`, JSON.stringify(persisted.serialize()));
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Restore the saved position for the given URL from sessionStorage.
|
|
132
|
+
*/
|
|
133
|
+
const restorePosition = (url, publication) => {
|
|
134
|
+
const saved = sessionStorage.getItem(`epub-locator:${url}`);
|
|
135
|
+
if (saved) {
|
|
136
|
+
const raw = JSON.parse(saved);
|
|
137
|
+
const parsed = Locator.deserialize(raw);
|
|
138
|
+
if (parsed) {
|
|
139
|
+
const href = parsed.href;
|
|
140
|
+
const idx = publication.readingOrder.findIndexWithHref(href) ?? -1;
|
|
141
|
+
if (idx >= 0) {
|
|
142
|
+
const prog = parsed.locations?.progression !== undefined &&
|
|
143
|
+
parsed.locations.progression >= 0 &&
|
|
144
|
+
parsed.locations.progression <= 1
|
|
145
|
+
? parsed.locations.progression
|
|
146
|
+
: 0;
|
|
147
|
+
return parsed.copyWithLocations({
|
|
148
|
+
position: idx + 1,
|
|
149
|
+
progression: prog,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const computePublicationChaptersSize = async (publication) => {
|
|
156
|
+
const resourceSizes = new Map();
|
|
157
|
+
await Promise.all(publication.readingOrder.items.map(async (item) => {
|
|
158
|
+
const href = (item?.href ?? '').split('#')[0];
|
|
159
|
+
if (!href || resourceSizes.has(href)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const length = await publication.get(item).length();
|
|
163
|
+
if (typeof length === 'number' && Number.isFinite(length) && length > 0) {
|
|
164
|
+
resourceSizes.set(href, length);
|
|
165
|
+
}
|
|
166
|
+
}));
|
|
167
|
+
return resourceSizes;
|
|
168
|
+
};
|
|
169
|
+
export { savePosition, restorePosition, generatePositions, buildWeightsFromZip, buildWeightsFromResolver, computeChapterProgressions, computePublicationChaptersSize, };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Locator } from '@readium/shared';
|
|
2
|
+
import type { TocEntry, ChapterBoundary } from '../../types/common';
|
|
3
|
+
/**
|
|
4
|
+
* Determine chapter title:
|
|
5
|
+
* 1. Progression-based lookup (single-file multi-chapter books)
|
|
6
|
+
* 2. Reading-order title (spine link fallback from TOC)
|
|
7
|
+
* 3. Fragment-aware TOC match
|
|
8
|
+
* 4. Base-href TOC fallback
|
|
9
|
+
*/
|
|
10
|
+
declare const getCurrentTitle: (locator: Locator, chapterProgressions: Map<string, ChapterBoundary[]>, tocLinks: TocEntry[]) => string;
|
|
11
|
+
export { getCurrentTitle };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determine chapter title:
|
|
3
|
+
* 1. Progression-based lookup (single-file multi-chapter books)
|
|
4
|
+
* 2. Reading-order title (spine link fallback from TOC)
|
|
5
|
+
* 3. Fragment-aware TOC match
|
|
6
|
+
* 4. Base-href TOC fallback
|
|
7
|
+
*/
|
|
8
|
+
const getCurrentTitle = (locator, chapterProgressions, tocLinks) => {
|
|
9
|
+
let chapterTitle = '';
|
|
10
|
+
const locBase = locator.href.split('#')[0];
|
|
11
|
+
const prog = locator.locations?.progression ?? 0;
|
|
12
|
+
const chapters = chapterProgressions.get(locBase);
|
|
13
|
+
if (chapters && chapters.length > 1) {
|
|
14
|
+
// Multi-chapter file → find the chapter whose startProg
|
|
15
|
+
// is the greatest that is ≤ current progression.
|
|
16
|
+
for (let i = chapters.length - 1; i >= 0; i--) {
|
|
17
|
+
if (prog >= chapters[i].startProg) {
|
|
18
|
+
chapterTitle = chapters[i].title;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!chapterTitle && locator.title) {
|
|
24
|
+
chapterTitle = locator.title;
|
|
25
|
+
}
|
|
26
|
+
if (!chapterTitle && tocLinks.length > 0) {
|
|
27
|
+
const tocMatch = tocLinks.find((entry) => locator.href === entry.link.href);
|
|
28
|
+
if (tocMatch) {
|
|
29
|
+
chapterTitle = tocMatch.title;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const baseMatch = tocLinks.find((entry) => entry.link.href.split('#')[0] === locBase);
|
|
33
|
+
if (baseMatch) {
|
|
34
|
+
chapterTitle = baseMatch.title;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return chapterTitle;
|
|
39
|
+
};
|
|
40
|
+
export { getCurrentTitle };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
import type { EpubNavigator } from '@readium/navigator';
|
|
3
|
+
import type { ReadiumWordDecorationsController } from '../../types/common';
|
|
4
|
+
export declare const createReadiumWordDecorationsController: (navigatorRef: Ref<EpubNavigator | null>, languageRef: () => string | undefined) => ReadiumWordDecorationsController;
|