@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.
- package/dist/ar-DyHX_uy2.js +7 -0
- package/dist/da-Dct0PS3E.js +7 -0
- package/dist/fr-C5HEel98.js +7 -0
- package/dist/index.js +7001 -6154
- package/dist/index.umd.cjs +1571 -39
- package/dist/it-DFOBoXGy.js +7 -0
- package/dist/pt_PT-Di3sVjze.js +7 -0
- package/dist/sv-BfzAFsVN.js +7 -0
- package/package.json +4 -2
- package/src/dom/_readium_executionCleanup.js +13 -0
- package/src/dom/_readium_executionPrevention.js +65 -0
- package/src/dom/_readium_webpubExecution.js +4 -0
- package/src/epub/EpubNavigator.ts +26 -2
- package/src/epub/frame/FrameBlobBuilder.ts +37 -131
- package/src/epub/frame/FramePoolManager.ts +34 -5
- package/src/epub/fxl/FXLFramePoolManager.ts +20 -2
- package/src/helpers/minify.ts +14 -0
- package/src/index.ts +2 -1
- package/src/injection/Injectable.ts +85 -0
- package/src/injection/Injector.ts +356 -0
- package/src/injection/epubInjectables.ts +90 -0
- package/src/injection/index.ts +2 -0
- package/src/injection/webpubInjectables.ts +59 -0
- package/src/webpub/WebPubBlobBuilder.ts +19 -80
- package/src/webpub/WebPubFramePoolManager.ts +29 -4
- package/src/webpub/WebPubNavigator.ts +15 -1
- package/types/src/epub/EpubNavigator.d.ts +3 -0
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +7 -4
- package/types/src/epub/frame/FramePoolManager.d.ts +3 -1
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +3 -1
- package/types/src/helpers/minify.d.ts +12 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/injection/Injectable.d.ts +68 -0
- package/types/src/injection/Injector.d.ts +22 -0
- package/types/src/injection/epubInjectables.d.ts +6 -0
- package/types/src/injection/index.d.ts +2 -0
- package/types/src/injection/webpubInjectables.d.ts +5 -0
- package/types/src/webpub/WebPubBlobBuilder.d.ts +7 -3
- package/types/src/webpub/WebPubFramePoolManager.d.ts +3 -1
- package/types/src/webpub/WebPubNavigator.d.ts +3 -0
- package/types/src/epub/preferences/guards.d.ts +0 -9
- package/types/src/web/WebPubBlobBuilder.d.ts +0 -10
- package/types/src/web/WebPubFrameManager.d.ts +0 -20
- package/types/src/web/WebPubNavigator.d.ts +0 -48
- package/types/src/web/index.d.ts +0 -3
- 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(
|
|
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(
|
|
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
|
@@ -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,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
|
+
}
|