@readium/navigator 2.2.7 → 2.2.9

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 (46) hide show
  1. package/dist/ar-DyHX_uy2.js +7 -0
  2. package/dist/da-Dct0PS3E.js +7 -0
  3. package/dist/fr-C5HEel98.js +7 -0
  4. package/dist/index.js +7001 -6154
  5. package/dist/index.umd.cjs +1571 -39
  6. package/dist/it-DFOBoXGy.js +7 -0
  7. package/dist/pt_PT-Di3sVjze.js +7 -0
  8. package/dist/sv-BfzAFsVN.js +7 -0
  9. package/package.json +4 -2
  10. package/src/dom/_readium_executionCleanup.js +13 -0
  11. package/src/dom/_readium_executionPrevention.js +65 -0
  12. package/src/dom/_readium_webpubExecution.js +4 -0
  13. package/src/epub/EpubNavigator.ts +26 -2
  14. package/src/epub/frame/FrameBlobBuilder.ts +37 -131
  15. package/src/epub/frame/FramePoolManager.ts +34 -5
  16. package/src/epub/fxl/FXLFramePoolManager.ts +20 -2
  17. package/src/helpers/minify.ts +14 -0
  18. package/src/index.ts +2 -1
  19. package/src/injection/Injectable.ts +85 -0
  20. package/src/injection/Injector.ts +356 -0
  21. package/src/injection/epubInjectables.ts +90 -0
  22. package/src/injection/index.ts +2 -0
  23. package/src/injection/webpubInjectables.ts +59 -0
  24. package/src/webpub/WebPubBlobBuilder.ts +19 -80
  25. package/src/webpub/WebPubFramePoolManager.ts +29 -4
  26. package/src/webpub/WebPubNavigator.ts +15 -1
  27. package/types/src/epub/EpubNavigator.d.ts +3 -0
  28. package/types/src/epub/frame/FrameBlobBuilder.d.ts +7 -4
  29. package/types/src/epub/frame/FramePoolManager.d.ts +3 -1
  30. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +3 -1
  31. package/types/src/helpers/minify.d.ts +12 -0
  32. package/types/src/index.d.ts +1 -0
  33. package/types/src/injection/Injectable.d.ts +68 -0
  34. package/types/src/injection/Injector.d.ts +22 -0
  35. package/types/src/injection/epubInjectables.d.ts +6 -0
  36. package/types/src/injection/index.d.ts +2 -0
  37. package/types/src/injection/webpubInjectables.d.ts +5 -0
  38. package/types/src/webpub/WebPubBlobBuilder.d.ts +7 -3
  39. package/types/src/webpub/WebPubFramePoolManager.d.ts +3 -1
  40. package/types/src/webpub/WebPubNavigator.d.ts +3 -0
  41. package/types/src/epub/preferences/guards.d.ts +0 -9
  42. package/types/src/web/WebPubBlobBuilder.d.ts +0 -10
  43. package/types/src/web/WebPubFrameManager.d.ts +0 -20
  44. package/types/src/web/WebPubNavigator.d.ts +0 -48
  45. package/types/src/web/index.d.ts +0 -3
  46. package/types/src/webpub/css/WebPubStylesheet.d.ts +0 -1
@@ -6,6 +6,7 @@ import { FXLFrameManager } from "./FXLFrameManager";
6
6
  import { FXLPeripherals } from "./FXLPeripherals";
7
7
  import { FXLSpreader, Orientation, Spread } from "./FXLSpreader";
8
8
  import { VisualNavigatorViewport } from "../../Navigator";
9
+ import { Injector } from "../../injection/Injector";
9
10
 
10
11
  const UPPER_BOUNDARY = 8;
11
12
  const LOWER_BOUNDARY = 5;
@@ -26,6 +27,7 @@ export class FXLFramePoolManager {
26
27
  private readonly delayedTimeout: Map<string, number> = new Map();
27
28
  private currentBaseURL: string | undefined;
28
29
  private previousFrames: FXLFrameManager[] = [];
30
+ private readonly injector: Injector | null = null;
29
31
 
30
32
  // NEW
31
33
  private readonly bookElement: HTMLDivElement;
@@ -44,10 +46,16 @@ export class FXLFramePoolManager {
44
46
  // private readonly pages: FXLFrameManager[] = [];
45
47
  public readonly peripherals: FXLPeripherals;
46
48
 
47
- constructor(container: HTMLElement, positions: Locator[], pub: Publication) {
49
+ constructor(
50
+ container: HTMLElement,
51
+ positions: Locator[],
52
+ pub: Publication,
53
+ injector?: Injector | null
54
+ ) {
48
55
  this.container = container;
49
56
  this.positions = positions;
50
57
  this.pub = pub;
58
+ this.injector = injector ?? null;
51
59
  this.spreadPresentation = pub.metadata.otherMetadata?.spread || Spread.auto;
52
60
 
53
61
  if(this.pub.metadata.effectiveReadingProgression !== ReadingProgression.rtl && this.pub.metadata.effectiveReadingProgression !== ReadingProgression.ltr)
@@ -393,6 +401,9 @@ export class FXLFramePoolManager {
393
401
  // Revoke all blobs
394
402
  this.blobs.forEach(v => URL.revokeObjectURL(v));
395
403
 
404
+ // Clean up injector if it exists
405
+ this.injector?.dispose();
406
+
396
407
  // Empty container of elements
397
408
  this.container.childNodes.forEach(v => {
398
409
  if(v.nodeType === Node.ELEMENT_NODE || v.nodeType === Node.TEXT_NODE) v.remove();
@@ -495,7 +506,14 @@ export class FXLFramePoolManager {
495
506
  const itm = pub.readingOrder.items[index];
496
507
  if(!itm) return; // TODO throw?
497
508
  if(!this.blobs.has(href)) {
498
- const blobBuilder = new FrameBlobBuider(pub, this.currentBaseURL || "", itm);
509
+ const blobBuilder = new FrameBlobBuider(
510
+ pub,
511
+ this.currentBaseURL || "",
512
+ itm,
513
+ {
514
+ injector: this.injector
515
+ }
516
+ );
499
517
  const blobURL = await blobBuilder.build(true);
500
518
  this.blobs.set(href, blobURL);
501
519
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Utilities for processing CSS and JavaScript content
3
+ */
4
+
5
+ /**
6
+ * Minifies JavaScript by removing comments and normalizing whitespace
7
+ */
8
+ export const stripJS = (source: string) => source.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\n/g, "").replace(/\s+/g, " ");
9
+
10
+ /**
11
+ * Minifies CSS by removing comments and normalizing whitespace
12
+ * Note: URL resolution should be handled by the caller with correct context
13
+ */
14
+ export const stripCSS = (source: string) => source.replace(/\/\*(?:(?!\*\/)[\s\S])*\*\/|[\r\n\t]+/g, "").replace(/ {2,}/g, " ");
package/src/index.ts CHANGED
@@ -4,4 +4,5 @@ export * from './epub';
4
4
  export * from './audio';
5
5
  export * from './helpers';
6
6
  export * from './preferences';
7
- export * from './css';
7
+ export * from './css';
8
+ export * from './injection';
@@ -0,0 +1,85 @@
1
+ import { Link } from "@readium/shared";
2
+
3
+ type ForbiddenAttributes = "type" | "rel" | "href" | "src";
4
+ type AllowedAttributes = {
5
+ [K in string]: K extends ForbiddenAttributes
6
+ ? never
7
+ : (string | boolean | undefined);
8
+ } & {
9
+ [K in ForbiddenAttributes]?: never;
10
+ };
11
+
12
+ export interface IBaseInjectable {
13
+ id?: string;
14
+ target?: "head" | "body";
15
+ type?: string;
16
+ condition?: (doc: Document) => boolean;
17
+
18
+ // Extra attributes - type and rel are forbidden here since they are at root
19
+ attributes?: AllowedAttributes;
20
+ }
21
+
22
+ export interface IScriptInjectable extends IBaseInjectable {
23
+ as: "script";
24
+ rel?: never; // Scripts don't have rel
25
+ }
26
+
27
+ export interface ILinkInjectable extends IBaseInjectable {
28
+ as: "link";
29
+ rel: string;
30
+ }
31
+
32
+ export interface IUrlInjectable {
33
+ url: string;
34
+ }
35
+
36
+ export interface IBlobInjectable {
37
+ blob: Blob;
38
+ }
39
+
40
+ export type IInjectable = (IScriptInjectable | ILinkInjectable) & (IUrlInjectable | IBlobInjectable);
41
+
42
+ /**
43
+ * Defines a rule for resource injection, specifying which resources to inject into which documents.
44
+ */
45
+ export interface IInjectableRule {
46
+ /**
47
+ * List of resource URLs or patterns that this rule applies to.
48
+ * Can be exact URLs or patterns with wildcards.
49
+ */
50
+ resources: Array<string | RegExp>;
51
+
52
+ /**
53
+ * Resources to inject at the beginning of the target (in array order)
54
+ */
55
+ prepend?: IInjectable[];
56
+
57
+ /**
58
+ * Resources to inject at the end of the target (in array order)
59
+ */
60
+ append?: IInjectable[];
61
+ }
62
+
63
+ export interface IInjectablesConfig {
64
+ rules: IInjectableRule[];
65
+ allowedDomains?: string[];
66
+ }
67
+
68
+ export interface IInjector {
69
+ /**
70
+ * Injects resources into a document based on matching rules
71
+ * @param doc The document to inject resources into
72
+ * @param link The link being loaded, used to match against injection rules
73
+ */
74
+ injectForDocument(doc: Document, link: Link): Promise<void>;
75
+
76
+ /**
77
+ * Cleans up any resources used by the injector
78
+ */
79
+ dispose(): void;
80
+
81
+ /**
82
+ * Get the list of allowed domains
83
+ */
84
+ getAllowedDomains(): string[]
85
+ }
@@ -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
+ }