@readium/navigator 2.2.6 → 2.2.8

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 (47) hide show
  1. package/dist/ar-DyHX_uy2-DyHX_uy2-DyHX_uy2.js +7 -0
  2. package/dist/ar-DyHX_uy2-DyHX_uy2.js +7 -0
  3. package/dist/da-Dct0PS3E-Dct0PS3E-Dct0PS3E.js +7 -0
  4. package/dist/da-Dct0PS3E-Dct0PS3E.js +7 -0
  5. package/dist/fr-C5HEel98-C5HEel98-C5HEel98.js +7 -0
  6. package/dist/fr-C5HEel98-C5HEel98.js +7 -0
  7. package/dist/index.js +4389 -2401
  8. package/dist/index.umd.cjs +1571 -39
  9. package/dist/it-DFOBoXGy-DFOBoXGy-DFOBoXGy.js +7 -0
  10. package/dist/it-DFOBoXGy-DFOBoXGy.js +7 -0
  11. package/dist/pt_PT-Di3sVjze-Di3sVjze-Di3sVjze.js +7 -0
  12. package/dist/pt_PT-Di3sVjze-Di3sVjze.js +7 -0
  13. package/dist/sv-BfzAFsVN-BfzAFsVN-BfzAFsVN.js +7 -0
  14. package/dist/sv-BfzAFsVN-BfzAFsVN.js +7 -0
  15. package/package.json +6 -5
  16. package/src/dom/_readium_cssSelectorGenerator.js +1 -0
  17. package/src/dom/_readium_executionCleanup.js +13 -0
  18. package/src/dom/_readium_executionPrevention.js +65 -0
  19. package/src/dom/_readium_webpubExecution.js +4 -0
  20. package/src/epub/EpubNavigator.ts +26 -2
  21. package/src/epub/frame/FrameBlobBuilder.ts +37 -130
  22. package/src/epub/frame/FramePoolManager.ts +34 -5
  23. package/src/epub/fxl/FXLFramePoolManager.ts +20 -2
  24. package/src/helpers/minify.ts +14 -0
  25. package/src/index.ts +2 -1
  26. package/src/injection/Injectable.ts +85 -0
  27. package/src/injection/Injector.ts +356 -0
  28. package/src/injection/epubInjectables.ts +90 -0
  29. package/src/injection/index.ts +2 -0
  30. package/src/injection/webpubInjectables.ts +59 -0
  31. package/src/webpub/WebPubBlobBuilder.ts +19 -76
  32. package/src/webpub/WebPubFramePoolManager.ts +29 -4
  33. package/src/webpub/WebPubNavigator.ts +15 -1
  34. package/types/src/epub/EpubNavigator.d.ts +3 -0
  35. package/types/src/epub/frame/FrameBlobBuilder.d.ts +7 -4
  36. package/types/src/epub/frame/FramePoolManager.d.ts +3 -1
  37. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +3 -1
  38. package/types/src/helpers/minify.d.ts +12 -0
  39. package/types/src/index.d.ts +1 -0
  40. package/types/src/injection/Injectable.d.ts +68 -0
  41. package/types/src/injection/Injector.d.ts +22 -0
  42. package/types/src/injection/epubInjectables.d.ts +6 -0
  43. package/types/src/injection/index.d.ts +2 -0
  44. package/types/src/injection/webpubInjectables.d.ts +5 -0
  45. package/types/src/webpub/WebPubBlobBuilder.d.ts +7 -3
  46. package/types/src/webpub/WebPubFramePoolManager.d.ts +3 -1
  47. package/types/src/webpub/WebPubNavigator.d.ts +3 -0
@@ -0,0 +1,356 @@
1
+ import { IInjectableRule, IInjectable, IInjector, IInjectablesConfig } from "./Injectable";
2
+ import { Link } from "@readium/shared";
3
+
4
+ const inferTypeFromResource = (resource: IInjectable): string | undefined => {
5
+ // If blob has a type, use it
6
+ if ("blob" in resource && resource.blob.type) {
7
+ return resource.blob.type;
8
+ }
9
+
10
+ // For scripts, default to text/javascript
11
+ if (resource.as === "script") {
12
+ return "text/javascript";
13
+ }
14
+
15
+ // For links, try to infer from URL extension
16
+ if (resource.as === "link" && "url" in resource) {
17
+ const url = resource.url.toLowerCase();
18
+ if (url.endsWith(".css")) return "text/css";
19
+ if ([".js", ".mjs", ".cjs"].some(ext => url.endsWith(ext))) return "text/javascript";
20
+ }
21
+
22
+ return undefined;
23
+ };
24
+
25
+ const applyAttributes = (element: HTMLElement, resource: IInjectable): void => {
26
+ // Apply extra attributes, filtering out root-level properties
27
+ if (resource.attributes) {
28
+ Object.entries(resource.attributes).forEach(([key, value]) => {
29
+ // Skip root-level properties to prevent conflicts
30
+ if (key === "type" || key === "rel" || key === "href" || key === "src") {
31
+ return;
32
+ }
33
+
34
+ if (value !== undefined && value !== null) {
35
+ // Convert boolean attributes to proper HTML format
36
+ if (typeof value === "boolean") {
37
+ if (value) {
38
+ element.setAttribute(key, "");
39
+ }
40
+ } else {
41
+ element.setAttribute(key, value);
42
+ }
43
+ }
44
+ });
45
+ }
46
+ };
47
+
48
+ const scriptify = (doc: Document, resource: IInjectable, source: string): HTMLScriptElement => {
49
+ const s = doc.createElement("script");
50
+ s.dataset.readium = "true";
51
+
52
+ // Set the injectable ID if provided
53
+ if (resource.id) {
54
+ s.id = resource.id;
55
+ }
56
+
57
+ // Apply root-level type if provided
58
+ const finalType = resource.type || inferTypeFromResource(resource);
59
+ if (finalType) {
60
+ s.type = finalType;
61
+ }
62
+
63
+ // Apply extra attributes
64
+ applyAttributes(s, resource);
65
+
66
+ // Always set src from the processed URL
67
+ s.src = source;
68
+
69
+ return s;
70
+ };
71
+
72
+ const linkify = (doc: Document, resource: IInjectable, source: string): HTMLLinkElement => {
73
+ const s = doc.createElement("link");
74
+ s.dataset.readium = "true";
75
+
76
+ // Set the injectable ID if provided
77
+ if (resource.id) {
78
+ s.id = resource.id;
79
+ }
80
+
81
+ // Apply root-level rel if provided
82
+ if (resource.rel) {
83
+ s.rel = resource.rel;
84
+ }
85
+
86
+ const finalType = resource.type || inferTypeFromResource(resource);
87
+ if (finalType) {
88
+ s.type = finalType;
89
+ }
90
+
91
+ // Apply extra attributes
92
+ applyAttributes(s, resource);
93
+
94
+ // Always set href from the processed URL
95
+ s.href = source;
96
+
97
+ return s;
98
+ };
99
+
100
+ export class Injector implements IInjector {
101
+ private readonly blobStore: Map<string, { url: string; refCount: number }> = new Map();
102
+ private readonly createdBlobUrls: Set<string> = new Set();
103
+ private readonly rules: IInjectableRule[];
104
+ private readonly allowedDomains: string[] = [];
105
+ private injectableIdCounter = 0;
106
+
107
+ constructor(config: IInjectablesConfig) {
108
+ // Validate allowed domains - they should be proper URLs for external resources
109
+ this.allowedDomains = (config.allowedDomains || []).map(domain => {
110
+ try {
111
+ new URL(domain);
112
+ return domain;
113
+ } catch {
114
+ throw new Error(`Invalid allowed domain: "${domain}". Must be a valid URL (e.g., "https://fonts.googleapis.com").`);
115
+ }
116
+ });
117
+
118
+ // Assign IDs to injectables that don't have them
119
+ this.rules = config.rules.map(rule => {
120
+ const processedRule: IInjectableRule = { ...rule };
121
+
122
+ // Process prepend injectables (reverse to preserve order when prepending)
123
+ if (rule.prepend) {
124
+ processedRule.prepend = rule.prepend.map(injectable => ({
125
+ ...injectable,
126
+ id: injectable.id || `injectable-${this.injectableIdCounter++}`
127
+ })).reverse(); // Reverse here so we can process normally later
128
+ }
129
+
130
+ // Process append injectables (keep original order)
131
+ if (rule.append) {
132
+ processedRule.append = rule.append.map(injectable => ({
133
+ ...injectable,
134
+ id: injectable.id || `injectable-${this.injectableIdCounter++}`
135
+ }));
136
+ }
137
+
138
+ return processedRule;
139
+ });
140
+ }
141
+
142
+ public dispose(): void {
143
+ // Cleanup any created blob URLs
144
+ for (const url of this.createdBlobUrls) {
145
+ try {
146
+ URL.revokeObjectURL(url);
147
+ } catch (error) {
148
+ console.warn("Failed to revoke blob URL:", url, error);
149
+ }
150
+ }
151
+ this.createdBlobUrls.clear();
152
+ }
153
+
154
+ public getAllowedDomains(): string[] {
155
+ return [...this.allowedDomains]; // Return a copy to prevent external modification
156
+ }
157
+
158
+ public async injectForDocument(doc: Document, link: Link): Promise<void> {
159
+ for (const rule of this.rules) {
160
+ if (this.matchesRule(rule, link)) {
161
+ await this.applyRule(doc, rule);
162
+ }
163
+ }
164
+ }
165
+
166
+ private matchesRule(rule: IInjectableRule, link: Link): boolean {
167
+ // Use the original href from the publication, not the resolved blob URL
168
+ const originalHref = link.href;
169
+
170
+ return rule.resources.some(pattern => {
171
+ if (pattern instanceof RegExp) {
172
+ return pattern.test(originalHref);
173
+ }
174
+ return originalHref === pattern;
175
+ });
176
+ }
177
+
178
+ private async getOrCreateBlobUrl(resource: IInjectable): Promise<string> {
179
+ // Use the injectable ID as the cache key
180
+ const cacheKey = resource.id!; // ID is guaranteed to exist after constructor
181
+
182
+ if (this.blobStore.has(cacheKey)) {
183
+ const entry = this.blobStore.get(cacheKey)!;
184
+ entry.refCount++;
185
+ return entry.url;
186
+ }
187
+
188
+ if ("blob" in resource) {
189
+ const url = URL.createObjectURL(resource.blob);
190
+ this.blobStore.set(cacheKey, { url, refCount: 1 });
191
+ this.createdBlobUrls.add(url);
192
+ return url;
193
+ }
194
+
195
+ throw new Error("Resource must have a blob property");
196
+ }
197
+
198
+ public async releaseBlobUrl(url: string): Promise<void> {
199
+ if (!this.createdBlobUrls.has(url)) return;
200
+
201
+ const entry = Array.from(this.blobStore.values())
202
+ .find(entry => entry.url === url);
203
+
204
+ if (entry) {
205
+ entry.refCount--;
206
+ if (entry.refCount <= 0) {
207
+ URL.revokeObjectURL(url);
208
+ this.createdBlobUrls.delete(url);
209
+ // Remove from blobStore
210
+ for (const [key, value] of this.blobStore.entries()) {
211
+ if (value.url === url) {
212
+ this.blobStore.delete(key);
213
+ break;
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ private async getResourceUrl(resource: IInjectable, doc: Document): Promise<string> {
221
+ if ("url" in resource) {
222
+ const resolvedUrl = new URL(resource.url, doc.baseURI).toString();
223
+ if (!this.isValidUrl(resolvedUrl, doc)) {
224
+ throw new Error(`Invalid URL: Only HTTPS, data:, blob:, or localhost HTTP URLs are allowed. Got: ${resource.url}`);
225
+ }
226
+ return resolvedUrl;
227
+ } else {
228
+ return this.getOrCreateBlobUrl(resource);
229
+ }
230
+ }
231
+
232
+ private createPreloadLink(doc: Document, resource: IInjectable, url: string): void {
233
+ if (resource.as !== "link" || resource.rel !== "preload") return;
234
+
235
+ // Create a new resource object with preload attributes
236
+ const preloadResource: IInjectable = {
237
+ ...resource,
238
+ rel: "preload",
239
+ attributes: {
240
+ ...resource.attributes,
241
+ as: resource.as
242
+ }
243
+ };
244
+
245
+ const preloadLink = linkify(doc, preloadResource, url);
246
+ doc.head.appendChild(preloadLink);
247
+ }
248
+
249
+ private createElement(doc: Document, resource: IInjectable, source: string): HTMLElement {
250
+ if (resource.as === "script") {
251
+ return scriptify(doc, resource, source);
252
+ }
253
+ if (resource.as === "link") {
254
+ return linkify(doc, resource, source);
255
+ }
256
+ throw new Error(`Unsupported element type: ${(resource as any).as}`);
257
+ }
258
+
259
+ private async applyRule(doc: Document, rule: IInjectableRule): Promise<void> {
260
+ const createdElements: { element: HTMLElement; url: string }[] = [];
261
+
262
+ // Collect all injectables that pass their conditions before modifying the document
263
+ const prependInjectables = rule.prepend ? rule.prepend.filter(resource =>
264
+ !resource.condition || resource.condition(doc)
265
+ ) : [];
266
+
267
+ const appendInjectables = rule.append ? rule.append.filter(resource =>
268
+ !resource.condition || resource.condition(doc)
269
+ ) : [];
270
+
271
+ try {
272
+ // Process prepend injectables first (already reversed in constructor)
273
+ for (const resource of prependInjectables) {
274
+ await this.processInjectable(resource, doc, createdElements, "prepend");
275
+ }
276
+
277
+ // Process append injectables next (in order)
278
+ for (const resource of appendInjectables) {
279
+ await this.processInjectable(resource, doc, createdElements, "append");
280
+ }
281
+ } catch (error) {
282
+ // Clean up any created elements on error
283
+ for (const { element, url } of createdElements) {
284
+ try {
285
+ element.remove();
286
+ await this.releaseBlobUrl(url);
287
+ } catch (cleanupError) {
288
+ console.error("Error during cleanup:", cleanupError);
289
+ }
290
+ }
291
+ throw error;
292
+ }
293
+ }
294
+
295
+ private async processInjectable(
296
+ resource: IInjectable,
297
+ doc: Document,
298
+ createdElements: { element: HTMLElement; url: string }[],
299
+ position: "prepend" | "append"
300
+ ): Promise<void> {
301
+ const target = resource.target === "body" ? doc.body : doc.head;
302
+ if (!target) return;
303
+
304
+ let url: string | null = null;
305
+ try {
306
+ url = await this.getResourceUrl(resource, doc);
307
+
308
+ if (resource.rel === "preload" && "url" in resource) {
309
+ this.createPreloadLink(doc, resource, url);
310
+ } else {
311
+ const element = this.createElement(doc, resource, url);
312
+ createdElements.push({ element, url });
313
+
314
+ if (position === "prepend") {
315
+ target.prepend(element);
316
+ } else {
317
+ target.append(element);
318
+ }
319
+ }
320
+ } catch (error) {
321
+ console.error("Failed to process resource:", error);
322
+ if (url && "blob" in resource) {
323
+ await this.releaseBlobUrl(url);
324
+ }
325
+ throw error;
326
+ }
327
+ }
328
+
329
+ private isValidUrl(url: string, doc: Document): boolean {
330
+ try {
331
+ const parsed = new URL(url, doc.baseURI);
332
+
333
+ // Allow data URLs
334
+ if (parsed.protocol === "data:") return true;
335
+
336
+ // Allow blob URLs that we created
337
+ if (parsed.protocol === "blob:" && this.createdBlobUrls.has(url)) {
338
+ return true;
339
+ }
340
+
341
+ // Check against allowed domains if any are specified
342
+ if (this.allowedDomains.length > 0) {
343
+ const origin = parsed.origin;
344
+ return this.allowedDomains.some(allowed => {
345
+ const allowedOrigin = new URL(allowed).origin;
346
+ return origin === allowedOrigin;
347
+ });
348
+ }
349
+
350
+ // No allowed domains specified - deny external URLs
351
+ return false;
352
+ } catch {
353
+ return false;
354
+ }
355
+ }
356
+ }
@@ -0,0 +1,90 @@
1
+ import { IInjectableRule, IInjectable } from "../injection/Injectable";
2
+ import { stripJS, stripCSS } from "../helpers/minify";
3
+ import { Metadata, Layout } from "@readium/shared";
4
+
5
+ import readiumCSSAfter from "@readium/css/css/dist/ReadiumCSS-after.css?raw";
6
+ import readiumCSSBefore from "@readium/css/css/dist/ReadiumCSS-before.css?raw";
7
+ import readiumCSSDefault from "@readium/css/css/dist/ReadiumCSS-default.css?raw";
8
+
9
+ import cssSelectorGeneratorContent from "../dom/_readium_cssSelectorGenerator.js?raw";
10
+ import executionPreventionContent from "../dom/_readium_executionPrevention.js?raw";
11
+ import onloadProxyContent from "../dom/_readium_executionCleanup.js?raw";
12
+
13
+ /**
14
+ * Creates injectable rules for EPUB content documents
15
+ */
16
+ export function createReadiumEpubRules(metadata: Metadata): IInjectableRule[] {
17
+ const isFixedLayout = metadata.effectiveLayout === Layout.fixed;
18
+
19
+ // Core injectables that should be prepended
20
+ const prependInjectables: IInjectable[] = [
21
+ // CSS Selector Generator - always injected
22
+ {
23
+ id: "css-selector-generator",
24
+ as: "script",
25
+ target: "head",
26
+ blob: new Blob([stripJS(cssSelectorGeneratorContent)], { type: "text/javascript" })
27
+ },
28
+ // Execution Prevention - conditional (has executable scripts)
29
+ {
30
+ id: "execution-prevention",
31
+ as: "script",
32
+ target: "head",
33
+ blob: new Blob([stripJS(executionPreventionContent)], { type: "text/javascript" }),
34
+ condition: (doc: Document) => !!(doc.querySelector("script") || doc.querySelector("body[onload]:not(body[onload=''])"))
35
+ }
36
+ ];
37
+
38
+ // Core injectables that should be appended
39
+ const appendInjectables: IInjectable[] = [
40
+ // Onload Proxy - conditional (has executable scripts)
41
+ {
42
+ id: "onload-proxy",
43
+ as: "script",
44
+ target: "head",
45
+ blob: new Blob([stripJS(onloadProxyContent)], { type: "text/javascript" }),
46
+ condition: (doc: Document) => !!(doc.querySelector("script") || doc.querySelector("body[onload]:not(body[onload=''])"))
47
+ }
48
+ ];
49
+
50
+ // Only add Readium CSS for reflowable documents
51
+ if (!isFixedLayout) {
52
+ // Readium CSS Before - prepended for reflowable
53
+ prependInjectables.unshift({
54
+ id: "readium-css-before",
55
+ as: "link",
56
+ target: "head",
57
+ blob: new Blob([stripCSS(readiumCSSBefore)], { type: "text/css" }),
58
+ rel: "stylesheet"
59
+ });
60
+
61
+ // Readium CSS Default and After - appended for reflowable
62
+ appendInjectables.unshift(
63
+ // Readium CSS Default - only for reflowable AND no existing styles
64
+ {
65
+ id: "readium-css-default",
66
+ as: "link",
67
+ target: "head",
68
+ blob: new Blob([stripCSS(readiumCSSDefault)], { type: "text/css" }),
69
+ rel: "stylesheet",
70
+ condition: (doc: Document) => !(doc.querySelector("link[rel='stylesheet']") || doc.querySelector("style") || doc.querySelector("[style]:not([style=''])"))
71
+ },
72
+ // Readium CSS After - only for reflowable
73
+ {
74
+ id: "readium-css-after",
75
+ as: "link",
76
+ target: "head",
77
+ blob: new Blob([stripCSS(readiumCSSAfter)], { type: "text/css" }),
78
+ rel: "stylesheet"
79
+ }
80
+ );
81
+ }
82
+
83
+ return [
84
+ {
85
+ resources: [/\.xhtml$/, /\.html$/],
86
+ prepend: prependInjectables,
87
+ append: appendInjectables
88
+ }
89
+ ];
90
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./Injectable";
2
+ export * from "./Injector";
@@ -0,0 +1,59 @@
1
+ import { IInjectableRule, IInjectable } from "../injection/Injectable";
2
+ import { stripJS, stripCSS } from "../helpers/minify";
3
+
4
+ import readiumCSSWebPub from "@readium/css/css/dist/webPub/ReadiumCSS-webPub.css?raw";
5
+
6
+ import cssSelectorGeneratorContent from "../dom/_readium_cssSelectorGenerator.js?raw";
7
+ import webpubExecutionContent from "../dom/_readium_webpubExecution.js?raw";
8
+ import onloadProxyContent from "../dom/_readium_executionCleanup.js?raw";
9
+
10
+ /**
11
+ * Creates injectable rules for WebPub content documents
12
+ */
13
+ export function createReadiumWebPubRules(): IInjectableRule[] {
14
+ // Core injectables that should be prepended
15
+ const prependInjectables: IInjectable[] = [
16
+ // CSS Selector Generator - always injected
17
+ {
18
+ id: "css-selector-generator",
19
+ as: "script",
20
+ target: "head",
21
+ blob: new Blob([stripJS(cssSelectorGeneratorContent)], { type: "text/javascript" })
22
+ },
23
+ // WebPub Execution - always injected (sets up event blocking to false)
24
+ {
25
+ id: "webpub-execution",
26
+ as: "script",
27
+ target: "head",
28
+ blob: new Blob([stripJS(webpubExecutionContent)], { type: "text/javascript" })
29
+ }
30
+ ];
31
+
32
+ // Core injectables that should be appended
33
+ const appendInjectables: IInjectable[] = [
34
+ // Onload Proxy - conditional (has executable scripts)
35
+ {
36
+ id: "onload-proxy",
37
+ as: "script",
38
+ target: "head",
39
+ blob: new Blob([stripJS(onloadProxyContent)], { type: "text/javascript" }),
40
+ condition: (doc: Document) => !!(doc.querySelector("script") || doc.querySelector("body[onload]:not(body[onload=''])"))
41
+ },
42
+ // Readium CSS WebPub - always injected
43
+ {
44
+ id: "readium-css-webpub",
45
+ as: "link",
46
+ target: "head",
47
+ blob: new Blob([stripCSS(readiumCSSWebPub)], { type: "text/css" }),
48
+ rel: "stylesheet"
49
+ }
50
+ ];
51
+
52
+ return [
53
+ {
54
+ resources: [/\.xhtml$/, /\.html$/],
55
+ prepend: prependInjectables,
56
+ append: appendInjectables
57
+ }
58
+ ];
59
+ }
@@ -1,74 +1,27 @@
1
1
  import { Link, Publication } from "@readium/shared";
2
-
3
- // Readium CSS imports
4
- // The "?inline" query is to prevent some bundlers from injecting these into the page (e.g. vite)
5
- // @ts-ignore
6
- import readiumCSSWebPub from "@readium/css/css/dist/webPub/ReadiumCSS-webPub.css?inline";
7
-
8
- // Utilities
9
- const blobify = (source: string, type: string) => URL.createObjectURL(new Blob([source], { type }));
10
- const stripJS = (source: string) => source.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\n/g, "").replace(/\s+/g, " ");
11
- const stripCSS = (source: string) => source.replace(/\/\*(?:(?!\*\/)[\s\S])*\*\/|[\r\n\t]+/g, '').replace(/ {2,}/g, ' ')
12
- // Fully resolve absolute local URLs created by bundlers since it's going into a blob
13
- .replace(/url\((?!(https?:)?\/\/)("?)\/([^\)]+)/g, `url($2${window.location.origin}/$3`);
14
- const scriptify = (doc: Document, source: string) => {
15
- const s = doc.createElement("script");
16
- s.dataset.readium = "true";
17
- s.src = source.startsWith("blob:") ? source : blobify(source, "text/javascript");
18
- return s;
19
- }
20
- const styleify = (doc: Document, source: string) => {
21
- const s = doc.createElement("link");
22
- s.dataset.readium = "true";
23
- s.rel = "stylesheet";
24
- s.type = "text/css";
25
- s.href = source.startsWith("blob:") ? source : blobify(source, "text/css");
26
- return s;
27
- }
28
-
29
- type CacheFunction = () => string;
30
- const resourceBlobCache = new Map<string, string>();
31
- const cached = (key: string, cacher: CacheFunction) => {
32
- if(resourceBlobCache.has(key)) return resourceBlobCache.get(key)!;
33
- const value = cacher();
34
- resourceBlobCache.set(key, value);
35
- return value;
36
- };
37
-
38
- const cssSelectorGenerator = (doc: Document) => scriptify(doc, cached("css-selector-generator", () => blobify(
39
- "!function(t,e){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define([],e):\"object\"==typeof exports?exports._readium_cssSelectorGenerator=e():t._readium_cssSelectorGenerator=e()}(self,(()=>(()=>{\"use strict\";var t,e,n={d:(t,e)=>{for(var o in e)n.o(e,o)&&!n.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(t,\"__esModule\",{value:!0})}},o={};function r(t){return t&&t instanceof Element}function i(t=\"unknown problem\",...e){console.warn(`CssSelectorGenerator: ${t}`,...e)}n.r(o),n.d(o,{default:()=>z,getCssSelector:()=>U}),function(t){t.NONE=\"none\",t.DESCENDANT=\"descendant\",t.CHILD=\"child\"}(t||(t={})),function(t){t.id=\"id\",t.class=\"class\",t.tag=\"tag\",t.attribute=\"attribute\",t.nthchild=\"nthchild\",t.nthoftype=\"nthoftype\"}(e||(e={}));const c={selectors:[e.id,e.class,e.tag,e.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function u(t){return t instanceof RegExp}function s(t){return[\"string\",\"function\"].includes(typeof t)||u(t)}function l(t){return Array.isArray(t)?t.filter(s):[]}function a(t){const e=[Node.DOCUMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE,Node.ELEMENT_NODE];return function(t){return t instanceof Node}(t)&&e.includes(t.nodeType)}function f(t,e){if(a(t))return t.contains(e)||i(\"element root mismatch\",\"Provided root does not contain the element. This will most likely result in producing a fallback selector using element\'s real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will nto work as intended.\"),t;const n=e.getRootNode({composed:!1});return a(n)?(n!==document&&i(\"shadow root inferred\",\"You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended.\"),n):e.ownerDocument.querySelector(\":root\")}function d(t){return\"number\"==typeof t?t:Number.POSITIVE_INFINITY}function m(t=[]){const[e=[],...n]=t;return 0===n.length?e:n.reduce(((t,e)=>t.filter((t=>e.includes(t)))),e)}function p(t){return[].concat(...t)}function h(t){const e=t.map((t=>{if(u(t))return e=>t.test(e);if(\"function\"==typeof t)return e=>{const n=t(e);return\"boolean\"!=typeof n?(i(\"pattern matcher function invalid\",\"Provided pattern matching function does not return boolean. It\'s result will be ignored.\",t),!1):n};if(\"string\"==typeof t){const e=new RegExp(\"^\"+t.replace(\/[|\\\\{}()[\\]^$+?.]\/g,\"\\\\$&\").replace(\/\\*\/g,\".+\")+\"$\");return t=>e.test(t)}return i(\"pattern matcher invalid\",\"Pattern matching only accepts strings, regular expressions and\/or functions. This item is invalid and will be ignored.\",t),()=>!1}));return t=>e.some((e=>e(t)))}function g(t,e,n){const o=Array.from(f(n,t[0]).querySelectorAll(e));return o.length===t.length&&t.every((t=>o.includes(t)))}function y(t,e){e=null!=e?e:function(t){return t.ownerDocument.querySelector(\":root\")}(t);const n=[];let o=t;for(;r(o)&&o!==e;)n.push(o),o=o.parentElement;return n}function b(t,e){return m(t.map((t=>y(t,e))))}const N={[t.NONE]:{type:t.NONE,value:\"\"},[t.DESCENDANT]:{type:t.DESCENDANT,value:\" > \"},[t.CHILD]:{type:t.CHILD,value:\" \"}},S=new RegExp([\"^$\",\"\\\\s\"].join(\"|\")),E=new RegExp([\"^$\"].join(\"|\")),w=[e.nthoftype,e.tag,e.id,e.class,e.attribute,e.nthchild],v=h([\"class\",\"id\",\"ng-*\"]);function C({nodeName:t}){return`[${t}]`}function O({nodeName:t,nodeValue:e}){return`[${t}=\'${L(e)}\']`}function T(t){const e=Array.from(t.attributes).filter((e=>function({nodeName:t},e){const n=e.tagName.toLowerCase();return!([\"input\",\"option\"].includes(n)&&\"value\"===t||v(t))}(e,t)));return[...e.map(C),...e.map(O)]}function I(t){return(t.getAttribute(\"class\")||\"\").trim().split(\/\\s+\/).filter((t=>!E.test(t))).map((t=>`.${L(t)}`))}function x(t){const e=t.getAttribute(\"id\")||\"\",n=`#${L(e)}`,o=t.getRootNode({composed:!1});return!S.test(e)&&g([t],n,o)?[n]:[]}function j(t){const e=t.parentNode;if(e){const n=Array.from(e.childNodes).filter(r).indexOf(t);if(n>-1)return[`:nth-child(${n+1})`]}return[]}function A(t){return[L(t.tagName.toLowerCase())]}function D(t){const e=[...new Set(p(t.map(A)))];return 0===e.length||e.length>1?[]:[e[0]]}function $(t){const e=D([t])[0],n=t.parentElement;if(n){const o=Array.from(n.children).filter((t=>t.tagName.toLowerCase()===e)),r=o.indexOf(t);if(r>-1)return[`${e}:nth-of-type(${r+1})`]}return[]}function R(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){const n=[];let o=0,r=k(1);for(;r.length<=t.length&&o<e;)o+=1,n.push(r.map((e=>t[e]))),r=P(r,t.length-1);return n}function P(t=[],e=0){const n=t.length;if(0===n)return[];const o=[...t];o[n-1]+=1;for(let t=n-1;t>=0;t--)if(o[t]>e){if(0===t)return k(n+1);o[t-1]++,o[t]=o[t-1]+1}return o[n-1]>e?k(n+1):o}function k(t=1){return Array.from(Array(t).keys())}const _=\":\".charCodeAt(0).toString(16).toUpperCase(),M=\/[ !\"#$%&\'()\\[\\]{|}<>*+,.\/;=?@^`~\\\\]\/;function L(t=\"\"){var e,n;return null!==(n=null===(e=null===CSS||void 0===CSS?void 0:CSS.escape)||void 0===e?void 0:e.call(CSS,t))&&void 0!==n?n:function(t=\"\"){return t.split(\"\").map((t=>\":\"===t?`\\\\${_} `:M.test(t)?`\\\\${t}`:escape(t).replace(\/%\/g,\"\\\\\"))).join(\"\")}(t)}const q={tag:D,id:function(t){return 0===t.length||t.length>1?[]:x(t[0])},class:function(t){return m(t.map(I))},attribute:function(t){return m(t.map(T))},nthchild:function(t){return m(t.map(j))},nthoftype:function(t){return m(t.map($))}},F={tag:A,id:x,class:I,attribute:T,nthchild:j,nthoftype:$};function V(t){return t.includes(e.tag)||t.includes(e.nthoftype)?[...t]:[...t,e.tag]}function Y(t={}){const n=[...w];return t[e.tag]&&t[e.nthoftype]&&n.splice(n.indexOf(e.tag),1),n.map((e=>{return(o=t)[n=e]?o[n].join(\"\"):\"\";var n,o})).join(\"\")}function B(t,e,n=\"\",o){const r=function(t,e){return\"\"===e?t:function(t,e){return[...t.map((t=>e+\" \"+t)),...t.map((t=>e+\" > \"+t))]}(t,e)}(function(t,e,n){const o=function(t,e){const{blacklist:n,whitelist:o,combineWithinSelector:r,maxCombinations:i}=e,c=h(n),u=h(o);return function(t){const{selectors:e,includeTag:n}=t,o=[].concat(e);return n&&!o.includes(\"tag\")&&o.push(\"tag\"),o}(e).reduce(((e,n)=>{const o=function(t,e){var n;return(null!==(n=q[e])&&void 0!==n?n:()=>[])(t)}(t,n),s=function(t=[],e,n){return t.filter((t=>n(t)||!e(t)))}(o,c,u),l=function(t=[],e){return t.sort(((t,n)=>{const o=e(t),r=e(n);return o&&!r?-1:!o&&r?1:0}))}(s,u);return e[n]=r?R(l,{maxResults:i}):l.map((t=>[t])),e}),{})}(t,n),r=function(t,e){return function(t){const{selectors:e,combineBetweenSelectors:n,includeTag:o,maxCandidates:r}=t,i=n?R(e,{maxResults:r}):e.map((t=>[t]));return o?i.map(V):i}(e).map((e=>function(t,e){const n={};return t.forEach((t=>{const o=e[t];o.length>0&&(n[t]=o)})),function(t={}){let e=[];return Object.entries(t).forEach((([t,n])=>{e=n.flatMap((n=>0===e.length?[{[t]:n}]:e.map((e=>Object.assign(Object.assign({},e),{[t]:n})))))})),e}(n).map(Y)}(e,t))).filter((t=>t.length>0))}(o,n),i=p(r);return[...new Set(i)]}(t,o.root,o),n);for(const e of r)if(g(t,e,o.root))return e;return null}function G(t){return{value:t,include:!1}}function W({selectors:t,operator:n}){let o=[...w];t[e.tag]&&t[e.nthoftype]&&(o=o.filter((t=>t!==e.tag)));let r=\"\";return o.forEach((e=>{(t[e]||[]).forEach((({value:t,include:e})=>{e&&(r+=t)}))})),n.value+r}function H(n){return[\":root\",...y(n).reverse().map((n=>{const o=function(e,n,o=t.NONE){const r={};return n.forEach((t=>{Reflect.set(r,t,function(t,e){return F[e](t)}(e,t).map(G))})),{element:e,operator:N[o],selectors:r}}(n,[e.nthchild],t.DESCENDANT);return o.selectors.nthchild.forEach((t=>{t.include=!0})),o})).map(W)].join(\"\")}function U(t,n={}){const o=function(t){const e=(Array.isArray(t)?t:[t]).filter(r);return[...new Set(e)]}(t),i=function(t,n={}){const o=Object.assign(Object.assign({},c),n);return{selectors:(r=o.selectors,Array.isArray(r)?r.filter((t=>{return n=e,o=t,Object.values(n).includes(o);var n,o})):[]),whitelist:l(o.whitelist),blacklist:l(o.blacklist),root:f(o.root,t),combineWithinSelector:!!o.combineWithinSelector,combineBetweenSelectors:!!o.combineBetweenSelectors,includeTag:!!o.includeTag,maxCombinations:d(o.maxCombinations),maxCandidates:d(o.maxCandidates)};var r}(o[0],n);let u=\"\",s=i.root;function a(){return function(t,e,n=\"\",o){if(0===t.length)return null;const r=[t.length>1?t:[],...b(t,e).map((t=>[t]))];for(const t of r){const e=B(t,0,n,o);if(e)return{foundElements:t,selector:e}}return null}(o,s,u,i)}let m=a();for(;m;){const{foundElements:t,selector:e}=m;if(g(o,e,i.root))return e;s=t[0],u=e,m=a()}return o.length>1?o.map((t=>U(t,i))).join(\", \"):function(t){return t.map(H).join(\", \")}(o)}const z=U;return o})()));",
40
- "text/javascript"
41
- )));
42
-
43
- const readiumPropertiesScript = `
44
- window._readium_blockedEvents = [];
45
- window._readium_blockEvents = false; // WebPub doesn't need event blocking
46
- window._readium_eventBlocker = null;
47
- `;
48
-
49
- const rBefore = (doc: Document) => scriptify(doc, cached("webpub-js-before", () => blobify(stripJS(readiumPropertiesScript), "text/javascript")));
50
- const rAfter = (doc: Document) => scriptify(doc, cached("webpub-js-after", () => blobify(stripJS(`
51
- if(window.onload) window.onload = new Proxy(window.onload, {
52
- apply: function(target, receiver, args) {
53
- if(!window._readium_blockEvents) {
54
- Reflect.apply(target, receiver, args);
55
- return;
56
- }
57
- _readium_blockedEvents.push([0, target, receiver, args]);
58
- }
59
- });`), "text/javascript")));
2
+ import { Injector } from "../injection/Injector";
60
3
 
61
4
  export class WebPubBlobBuilder {
62
5
  private readonly item: Link;
63
6
  private readonly burl: string;
64
7
  private readonly pub: Publication;
65
8
  private readonly cssProperties?: { [key: string]: string };
66
-
67
- constructor(pub: Publication, baseURL: string, item: Link, cssProperties?: { [key: string]: string }) {
9
+ private readonly injector: Injector | null = null;
10
+
11
+ constructor(
12
+ pub: Publication,
13
+ baseURL: string,
14
+ item: Link,
15
+ options: {
16
+ cssProperties?: { [key: string]: string };
17
+ injector?: Injector | null;
18
+ }
19
+ ) {
68
20
  this.pub = pub;
69
21
  this.item = item;
70
22
  this.burl = item.toURL(baseURL) || "";
71
- this.cssProperties = cssProperties;
23
+ this.cssProperties = options.cssProperties;
24
+ this.injector = options.injector ?? null;
72
25
  }
73
26
 
74
27
  public async build(): Promise<string> {
@@ -92,14 +45,12 @@ export class WebPubBlobBuilder {
92
45
  const details = perror.querySelector("div");
93
46
  throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
94
47
  }
95
- return this.finalizeDOM(doc, this.burl, this.item.mediaType, txt, this.cssProperties);
96
- }
97
48
 
98
- private hasExecutable(doc: Document): boolean {
99
- return (
100
- !!doc.querySelector("script") ||
101
- !!doc.querySelector("body[onload]:not(body[onload=''])")
102
- );
49
+ // Apply resource injections if injection service is provided
50
+ if (this.injector) {
51
+ await this.injector.injectForDocument(doc, this.item);
52
+ }
53
+ return this.finalizeDOM(doc, this.burl, this.item.mediaType, txt, this.cssProperties);
103
54
  }
104
55
 
105
56
  private setProperties(cssProperties: { [key: string]: string }, doc: Document) {
@@ -112,9 +63,6 @@ export class WebPubBlobBuilder {
112
63
  private finalizeDOM(doc: Document, base: string | undefined, mediaType: any, txt?: string, cssProperties?: { [key: string]: string }): string {
113
64
  if(!doc) return "";
114
65
 
115
- // ReadiumCSS WebPub
116
- doc.head.appendChild(styleify(doc, cached("ReadiumCSS-webpub", () => blobify(stripCSS(readiumCSSWebPub), "text/css"))));
117
-
118
66
  if (cssProperties) {
119
67
  this.setProperties(cssProperties, doc);
120
68
  }
@@ -130,11 +78,6 @@ export class WebPubBlobBuilder {
130
78
  doc.head.firstChild!.before(b);
131
79
  }
132
80
 
133
- const hasExecutable = this.hasExecutable(doc);
134
- if(hasExecutable) doc.head.firstChild!.before(rBefore(doc));
135
- doc.head.firstChild!.before(cssSelectorGenerator(doc));
136
- if(hasExecutable) doc.head.appendChild(rAfter(doc));
137
-
138
81
  // Serialize properly based on content type
139
82
  let serializedContent: string;
140
83