@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.
- package/dist/ar-DyHX_uy2-DyHX_uy2-DyHX_uy2.js +7 -0
- package/dist/ar-DyHX_uy2-DyHX_uy2.js +7 -0
- package/dist/da-Dct0PS3E-Dct0PS3E-Dct0PS3E.js +7 -0
- package/dist/da-Dct0PS3E-Dct0PS3E.js +7 -0
- package/dist/fr-C5HEel98-C5HEel98-C5HEel98.js +7 -0
- package/dist/fr-C5HEel98-C5HEel98.js +7 -0
- package/dist/index.js +4389 -2401
- package/dist/index.umd.cjs +1571 -39
- package/dist/it-DFOBoXGy-DFOBoXGy-DFOBoXGy.js +7 -0
- package/dist/it-DFOBoXGy-DFOBoXGy.js +7 -0
- package/dist/pt_PT-Di3sVjze-Di3sVjze-Di3sVjze.js +7 -0
- package/dist/pt_PT-Di3sVjze-Di3sVjze.js +7 -0
- package/dist/sv-BfzAFsVN-BfzAFsVN-BfzAFsVN.js +7 -0
- package/dist/sv-BfzAFsVN-BfzAFsVN.js +7 -0
- package/package.json +6 -5
- package/src/dom/_readium_cssSelectorGenerator.js +1 -0
- 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 -130
- 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 -76
- 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
|
@@ -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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|