@readium/navigator 2.2.3 → 2.2.5
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/index.js +1565 -1661
- package/dist/index.umd.cjs +20 -266
- package/package.json +4 -4
- package/src/epub/css/Properties.ts +10 -1
- package/src/epub/css/ReadiumCSS.ts +3 -0
- package/src/epub/frame/FrameBlobBuilder.ts +39 -11
- package/src/epub/frame/FrameManager.ts +1 -0
- package/src/epub/fxl/FXLFrameManager.ts +1 -0
- package/src/epub/preferences/EpubDefaults.ts +8 -2
- package/src/epub/preferences/EpubPreferencesEditor.ts +24 -3
- package/src/epub/preferences/EpubSettings.ts +6 -2
- package/src/preferences/Types.ts +6 -0
- package/src/preferences/guards.ts +12 -0
- package/src/webpub/WebPubBlobBuilder.ts +17 -8
- package/src/webpub/WebPubNavigator.ts +9 -1
- package/src/webpub/css/Properties.ts +34 -1
- package/src/webpub/css/WebPubCSS.ts +10 -1
- package/src/webpub/css/index.ts +1 -2
- package/src/webpub/preferences/WebPubDefaults.ts +19 -2
- package/src/webpub/preferences/WebPubPreferences.ts +6 -0
- package/src/webpub/preferences/WebPubPreferencesEditor.ts +22 -0
- package/src/webpub/preferences/WebPubSettings.ts +23 -2
- package/types/src/epub/css/Properties.d.ts +3 -1
- package/types/src/epub/preferences/EpubDefaults.d.ts +3 -1
- package/types/src/epub/preferences/EpubSettings.d.ts +3 -1
- package/types/src/preferences/Types.d.ts +14 -0
- package/types/src/preferences/guards.d.ts +2 -0
- package/types/src/webpub/css/Properties.d.ts +15 -1
- package/types/src/webpub/css/WebPubCSS.d.ts +3 -1
- package/types/src/webpub/css/index.d.ts +0 -1
- package/types/src/webpub/preferences/WebPubDefaults.d.ts +7 -1
- package/types/src/webpub/preferences/WebPubPreferences.d.ts +4 -0
- package/types/src/webpub/preferences/WebPubPreferencesEditor.d.ts +2 -0
- package/types/src/webpub/preferences/WebPubSettings.d.ts +7 -1
- package/src/webpub/css/WebPubStylesheet.ts +0 -205
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@readium/navigator",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Next generation SDK for publications in Web Apps",
|
|
6
6
|
"author": "readium",
|
|
@@ -49,16 +49,16 @@
|
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@laynezh/vite-plugin-lib-assets": "^2.1.0",
|
|
52
|
-
"@readium/css": "2.0.0-beta.
|
|
52
|
+
"@readium/css": "2.0.0-beta.24",
|
|
53
53
|
"@readium/navigator-html-injectables": "workspace:*",
|
|
54
54
|
"@readium/shared": "workspace:*",
|
|
55
55
|
"@types/path-browserify": "^1.0.3",
|
|
56
56
|
"css-selector-generator": "^3.6.9",
|
|
57
57
|
"path-browserify": "^1.0.1",
|
|
58
58
|
"tslib": "^2.8.1",
|
|
59
|
-
"typescript": "^5.9.
|
|
59
|
+
"typescript": "^5.9.3",
|
|
60
60
|
"typescript-plugin-css-modules": "^5.2.0",
|
|
61
61
|
"user-agent-data-types": "^0.4.2",
|
|
62
|
-
"vite": "^7.
|
|
62
|
+
"vite": "^7.2.4"
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TextAlignment } from "../../preferences/Types";
|
|
1
|
+
import { ExperimentKey, experiments, TextAlignment } from "../../preferences/Types";
|
|
2
2
|
import {
|
|
3
3
|
BodyHyphens,
|
|
4
4
|
BoxSizing,
|
|
@@ -214,6 +214,7 @@ export interface IRSProperties {
|
|
|
214
214
|
textColor?: string | null;
|
|
215
215
|
typeScale?: TypeScale | null;
|
|
216
216
|
visitedColor?: string | null;
|
|
217
|
+
experiments?: Array<ExperimentKey> | null;
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
export class RSProperties extends Properties {
|
|
@@ -258,6 +259,7 @@ export class RSProperties extends Properties {
|
|
|
258
259
|
textColor: string | null;
|
|
259
260
|
typeScale: TypeScale | null;
|
|
260
261
|
visitedColor: string | null;
|
|
262
|
+
experiments: Array<ExperimentKey> | null;
|
|
261
263
|
|
|
262
264
|
constructor(props: IRSProperties) {
|
|
263
265
|
super();
|
|
@@ -302,6 +304,7 @@ export class RSProperties extends Properties {
|
|
|
302
304
|
this.textColor = props.textColor ?? null;
|
|
303
305
|
this.typeScale = props.typeScale ?? null;
|
|
304
306
|
this.visitedColor = props.visitedColor ?? null;
|
|
307
|
+
this.experiments = props.experiments ?? null;
|
|
305
308
|
}
|
|
306
309
|
|
|
307
310
|
toCSSProperties(): { [key: string]: string; } {
|
|
@@ -349,6 +352,12 @@ export class RSProperties extends Properties {
|
|
|
349
352
|
if (this.typeScale) cssProperties["--RS__typeScale"] = this.toUnitless(this.typeScale);
|
|
350
353
|
if (this.visitedColor) cssProperties["--RS__visitedColor"] = this.visitedColor;
|
|
351
354
|
|
|
355
|
+
if (this.experiments) {
|
|
356
|
+
this.experiments.forEach((exp) => {
|
|
357
|
+
cssProperties["--RS__" + exp] = experiments[exp].value;
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
|
|
352
361
|
return cssProperties;
|
|
353
362
|
}
|
|
354
363
|
}
|
|
@@ -54,6 +54,9 @@ export class ReadiumCSS {
|
|
|
54
54
|
if (settings.scrollPaddingTop !== this.rsProperties.scrollPaddingTop)
|
|
55
55
|
this.rsProperties.scrollPaddingTop = settings.scrollPaddingTop;
|
|
56
56
|
|
|
57
|
+
if (settings.experiments !== this.rsProperties.experiments)
|
|
58
|
+
this.rsProperties.experiments = settings.experiments;
|
|
59
|
+
|
|
57
60
|
// This has to be updated before pagination
|
|
58
61
|
// otherwise the metrics won’t be correct for line length
|
|
59
62
|
this.lineLengths.update({
|
|
@@ -50,7 +50,10 @@ const cssSelectorGenerator = (doc: Document) => scriptify(doc, cached("css-selec
|
|
|
50
50
|
|
|
51
51
|
// Note: we aren't blocking some of the events right now to try and be as nonintrusive as possible.
|
|
52
52
|
// For a more comprehensive implementation, see https://github.com/hackademix/noscript/blob/3a83c0e4a506f175e38b0342dad50cdca3eae836/src/content/syncFetchPolicy.js#L142
|
|
53
|
+
// The snippet of code at the beginning of this source is an attempt at defence against JS using persistent storage
|
|
53
54
|
const rBefore = (doc: Document) => scriptify(doc, cached("JS-Before", () => blobify(stripJS(`
|
|
55
|
+
const noop=()=>{},emptyObj={},emptyPromise=()=>Promise.resolve(void 0),fakeStorage={getItem:noop,setItem:noop,removeItem:noop,clear:noop,key:noop,length:0};["localStorage","sessionStorage"].forEach((e=>Object.defineProperty(window,e,{get:()=>fakeStorage,configurable:!0}))),Object.defineProperty(document,"cookie",{get:()=>"",set:noop,configurable:!0}),Object.defineProperty(window,"indexedDB",{get:()=>{},configurable:!0}),Object.defineProperty(window,"caches",{get:()=>emptyObj,configurable:!0}),Object.defineProperty(navigator,"storage",{get:()=>({persist:emptyPromise,persisted:emptyPromise,estimate:()=>Promise.resolve({quota:0,usage:0})}),configurable:!0}),Object.defineProperty(navigator,"serviceWorker",{get:()=>({register:emptyPromise,getRegistration:emptyPromise,ready:emptyPromise()}),configurable:!0});
|
|
56
|
+
|
|
54
57
|
window._readium_blockedEvents = [];
|
|
55
58
|
window._readium_blockEvents = true;
|
|
56
59
|
window._readium_eventBlocker = (e) => {
|
|
@@ -78,6 +81,24 @@ const rAfter = (doc: Document) => scriptify(doc, cached("JS-After", () => blobif
|
|
|
78
81
|
});`
|
|
79
82
|
), "text/javascript")));
|
|
80
83
|
|
|
84
|
+
const csp = (domains: string[]) => {
|
|
85
|
+
const d = domains.join(" ");
|
|
86
|
+
return [
|
|
87
|
+
// 'self' is useless because the document is loaded from a blob: URL
|
|
88
|
+
`upgrade-insecure-requests`,
|
|
89
|
+
`default-src ${d} blob:`,
|
|
90
|
+
`connect-src 'none'`, // No fetches to anywhere. TODO: change?
|
|
91
|
+
`script-src ${d} blob: 'unsafe-inline'`, // JS scripts
|
|
92
|
+
`style-src ${d} blob: 'unsafe-inline'`, // CSS styles
|
|
93
|
+
`img-src ${d} blob: data:`, // Images
|
|
94
|
+
`font-src ${d} blob: data:`, // Fonts
|
|
95
|
+
`object-src ${d} blob:`, // Despite not being recommended, still necessary in EPUBs for <object>
|
|
96
|
+
`child-src ${d}`, // <iframe>, web workers
|
|
97
|
+
`form-action 'none'`, // No form submissions
|
|
98
|
+
//`report-uri ?`,
|
|
99
|
+
].join("; ");
|
|
100
|
+
};
|
|
101
|
+
|
|
81
102
|
export default class FrameBlobBuider {
|
|
82
103
|
private readonly item: Link;
|
|
83
104
|
private readonly burl: string;
|
|
@@ -93,7 +114,7 @@ export default class FrameBlobBuider {
|
|
|
93
114
|
|
|
94
115
|
public async build(fxl = false): Promise<string> {
|
|
95
116
|
if(!this.item.mediaType.isHTML) {
|
|
96
|
-
if(this.item.mediaType.isBitmap) {
|
|
117
|
+
if(this.item.mediaType.isBitmap || this.item.mediaType.equals(MediaType.SVG)) {
|
|
97
118
|
return this.buildImageFrame();
|
|
98
119
|
} else
|
|
99
120
|
throw Error("Unsupported frame mediatype " + this.item.mediaType.string);
|
|
@@ -115,7 +136,7 @@ export default class FrameBlobBuider {
|
|
|
115
136
|
const details = perror.querySelector("div");
|
|
116
137
|
throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
|
|
117
138
|
}
|
|
118
|
-
return this.finalizeDOM(doc, this.burl, this.item.mediaType, fxl, this.cssProperties);
|
|
139
|
+
return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, fxl, this.cssProperties);
|
|
119
140
|
}
|
|
120
141
|
|
|
121
142
|
private buildImageFrame(): string {
|
|
@@ -126,7 +147,7 @@ export default class FrameBlobBuider {
|
|
|
126
147
|
simg.alt = this.item.title || "";
|
|
127
148
|
simg.decoding = "async";
|
|
128
149
|
doc.body.appendChild(simg);
|
|
129
|
-
return this.finalizeDOM(doc, this.burl, this.item.mediaType, true);
|
|
150
|
+
return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, true);
|
|
130
151
|
}
|
|
131
152
|
|
|
132
153
|
// Has JS that may have side-effects when the document is loaded, without any user interaction
|
|
@@ -159,7 +180,7 @@ export default class FrameBlobBuider {
|
|
|
159
180
|
}
|
|
160
181
|
}
|
|
161
182
|
|
|
162
|
-
private finalizeDOM(doc: Document, base: string | undefined, mediaType: MediaType, fxl = false, cssProperties?: { [key: string]: string }): string {
|
|
183
|
+
private finalizeDOM(doc: Document, root: string | undefined, base: string | undefined, mediaType: MediaType, fxl = false, cssProperties?: { [key: string]: string }): string {
|
|
163
184
|
if(!doc) return "";
|
|
164
185
|
|
|
165
186
|
// Inject styles
|
|
@@ -233,8 +254,8 @@ export default class FrameBlobBuider {
|
|
|
233
254
|
) {
|
|
234
255
|
document.documentElement.dir = this.pub.metadata.effectiveReadingProgression;
|
|
235
256
|
} */
|
|
236
|
-
|
|
237
|
-
if(base !== undefined) {
|
|
257
|
+
|
|
258
|
+
if (base !== undefined) {
|
|
238
259
|
// Set all URL bases. Very convenient!
|
|
239
260
|
const b = doc.createElement("base");
|
|
240
261
|
b.href = base;
|
|
@@ -244,17 +265,24 @@ export default class FrameBlobBuider {
|
|
|
244
265
|
|
|
245
266
|
// Inject script to prevent in-publication scripts from executing until we want them to
|
|
246
267
|
const hasExecutable = this.hasExecutable(doc);
|
|
247
|
-
if(hasExecutable) doc.head.firstChild!.before(rBefore(doc));
|
|
268
|
+
if (hasExecutable) doc.head.firstChild!.before(rBefore(doc));
|
|
248
269
|
doc.head.firstChild!.before(cssSelectorGenerator(doc)); // CSS selector utility
|
|
249
|
-
if(hasExecutable) doc.head.appendChild(rAfter(doc)); // Another execution prevention script
|
|
270
|
+
if (hasExecutable) doc.head.appendChild(rAfter(doc)); // Another execution prevention script
|
|
271
|
+
|
|
272
|
+
// Add CSP
|
|
273
|
+
const meta = doc.createElement("meta");
|
|
274
|
+
meta.httpEquiv = "Content-Security-Policy";
|
|
275
|
+
meta.content = csp(root ? [root] : []);
|
|
276
|
+
meta.dataset.readium = "true";
|
|
277
|
+
doc.head.firstChild!.before(meta);
|
|
250
278
|
|
|
251
279
|
|
|
252
280
|
// Make blob from doc
|
|
253
281
|
return URL.createObjectURL(
|
|
254
282
|
new Blob([new XMLSerializer().serializeToString(doc)], {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
283
|
+
type: mediaType.isHTML
|
|
284
|
+
? mediaType.string
|
|
285
|
+
: "application/xhtml+xml", // Fallback to XHTML
|
|
258
286
|
})
|
|
259
287
|
);
|
|
260
288
|
}
|
|
@@ -16,6 +16,7 @@ export class FrameManager {
|
|
|
16
16
|
|
|
17
17
|
constructor(source: string) {
|
|
18
18
|
this.frame = document.createElement("iframe");
|
|
19
|
+
this.frame.sandbox.value = "allow-same-origin allow-scripts";
|
|
19
20
|
this.frame.classList.add("readium-navigator-iframe");
|
|
20
21
|
this.frame.style.visibility = "hidden";
|
|
21
22
|
this.frame.style.setProperty("aria-hidden", "true");
|
|
@@ -23,6 +23,7 @@ export class FXLFrameManager {
|
|
|
23
23
|
this.peripherals = peripherals;
|
|
24
24
|
this.debugHref = debugHref;
|
|
25
25
|
this.frame = document.createElement("iframe");
|
|
26
|
+
this.frame.sandbox.value = "allow-same-origin allow-scripts";
|
|
26
27
|
this.frame.classList.add("readium-navigator-iframe");
|
|
27
28
|
this.frame.classList.add("blank");
|
|
28
29
|
this.frame.scrolling = "no";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ExperimentKey,
|
|
2
3
|
fontSizeRangeConfig,
|
|
3
4
|
fontWeightRangeConfig,
|
|
4
5
|
fontWidthRangeConfig,
|
|
@@ -8,13 +9,14 @@ import {
|
|
|
8
9
|
import {
|
|
9
10
|
ensureBoolean,
|
|
10
11
|
ensureEnumValue,
|
|
12
|
+
ensureExperiment,
|
|
11
13
|
ensureFilter,
|
|
12
14
|
ensureLessThanOrEqual,
|
|
13
15
|
ensureMoreThanOrEqual,
|
|
14
16
|
ensureNonNegative,
|
|
15
17
|
ensureString,
|
|
16
18
|
ensureValueInRange,
|
|
17
|
-
withFallback
|
|
19
|
+
withFallback
|
|
18
20
|
} from "../../preferences/guards";
|
|
19
21
|
|
|
20
22
|
import { sMLWithRequest } from "../../helpers";
|
|
@@ -59,7 +61,8 @@ export interface IEpubDefaults {
|
|
|
59
61
|
textColor?: string | null,
|
|
60
62
|
textNormalization?: boolean | null,
|
|
61
63
|
visitedColor?: string | null,
|
|
62
|
-
wordSpacing?: number | null
|
|
64
|
+
wordSpacing?: number | null,
|
|
65
|
+
experiments?: Array<ExperimentKey> | null
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
export class EpubDefaults {
|
|
@@ -103,6 +106,7 @@ export class EpubDefaults {
|
|
|
103
106
|
textNormalization: boolean | null;
|
|
104
107
|
visitedColor: string | null;
|
|
105
108
|
wordSpacing: number | null;
|
|
109
|
+
experiments: Array<ExperimentKey> | null;
|
|
106
110
|
|
|
107
111
|
constructor(defaults: IEpubDefaults) {
|
|
108
112
|
this.backgroundColor = ensureString(defaults.backgroundColor) || null;
|
|
@@ -153,5 +157,7 @@ export class EpubDefaults {
|
|
|
153
157
|
this.optimalLineLength = ensureNonNegative(defaults.optimalLineLength) || 65;
|
|
154
158
|
this.maximalLineLength = withFallback(ensureMoreThanOrEqual(defaults.maximalLineLength, this.optimalLineLength), 80);
|
|
155
159
|
this.minimalLineLength = withFallback(ensureLessThanOrEqual(defaults.minimalLineLength, this.optimalLineLength), 40);
|
|
160
|
+
|
|
161
|
+
this.experiments = ensureExperiment(defaults.experiments) || null;
|
|
156
162
|
}
|
|
157
163
|
}
|
|
@@ -256,9 +256,30 @@ export class EpubPreferencesEditor implements IPreferencesEditor {
|
|
|
256
256
|
return new BooleanPreference({
|
|
257
257
|
initialValue: this.preferences.ligatures,
|
|
258
258
|
effectiveValue: this.settings.ligatures || true,
|
|
259
|
-
isEffective:
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
isEffective: (() => {
|
|
260
|
+
// Always respect explicit null (disabled) preference
|
|
261
|
+
if (this.preferences.ligatures === null) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Disable for fixed layout
|
|
266
|
+
if (this.layout === Layout.fixed) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check for languages/scripts that should disable ligatures
|
|
271
|
+
// ReadiumCSS does not apply in CJK
|
|
272
|
+
const primaryLang = this.metadata?.languages?.[0]?.toLowerCase();
|
|
273
|
+
if (primaryLang) {
|
|
274
|
+
// Disable for Chinese, Japanese, Korean, and Traditional Mongolian (mn-Mong)
|
|
275
|
+
if (["zh", "ja", "ko", "mn-mong"].some(lang => primaryLang.startsWith(lang))) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Enable by default
|
|
281
|
+
return true;
|
|
282
|
+
})(),
|
|
262
283
|
onChange: (newValue: boolean | null | undefined) => {
|
|
263
284
|
this.updatePreference("ligatures", newValue || null);
|
|
264
285
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConfigurableSettings } from "../../preferences/Configurable";
|
|
2
|
-
import { TextAlignment } from "../../preferences/Types";
|
|
2
|
+
import { ExperimentKey, TextAlignment } from "../../preferences/Types";
|
|
3
3
|
import { EpubDefaults } from "./EpubDefaults";
|
|
4
4
|
import { EpubPreferences } from "./EpubPreferences";
|
|
5
5
|
|
|
@@ -45,7 +45,8 @@ export interface IEpubSettings {
|
|
|
45
45
|
textColor?: string | null,
|
|
46
46
|
textNormalization?: boolean | null,
|
|
47
47
|
visitedColor?: string | null,
|
|
48
|
-
wordSpacing?: number | null
|
|
48
|
+
wordSpacing?: number | null,
|
|
49
|
+
experiments?: Array<ExperimentKey> | null
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export class EpubSettings implements ConfigurableSettings {
|
|
@@ -89,6 +90,7 @@ export class EpubSettings implements ConfigurableSettings {
|
|
|
89
90
|
textNormalization: boolean | null;
|
|
90
91
|
visitedColor: string | null;
|
|
91
92
|
wordSpacing: number | null;
|
|
93
|
+
experiments: Array<ExperimentKey> | null;
|
|
92
94
|
|
|
93
95
|
constructor(preferences: EpubPreferences, defaults: EpubDefaults) {
|
|
94
96
|
this.backgroundColor = preferences.backgroundColor || defaults.backgroundColor || null;
|
|
@@ -229,5 +231,7 @@ export class EpubSettings implements ConfigurableSettings {
|
|
|
229
231
|
: defaults.wordSpacing !== undefined
|
|
230
232
|
? defaults.wordSpacing
|
|
231
233
|
: null;
|
|
234
|
+
|
|
235
|
+
this.experiments = defaults.experiments || null;
|
|
232
236
|
}
|
|
233
237
|
}
|
package/src/preferences/Types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ExperimentKey, experiments } from './Types';
|
|
2
|
+
|
|
1
3
|
export function ensureLessThanOrEqual<T extends number | null | undefined>(value: T, compareTo: T): T | undefined {
|
|
2
4
|
if (value === undefined || value === null) {
|
|
3
5
|
return value;
|
|
@@ -83,3 +85,13 @@ export function ensureValueInRange(value: number | null | undefined, range: [num
|
|
|
83
85
|
export function withFallback<T>(value: T | null | undefined, defaultValue: T | null): T | null {
|
|
84
86
|
return value === undefined ? defaultValue : value;
|
|
85
87
|
}
|
|
88
|
+
|
|
89
|
+
export function ensureExperiment(experimentsInput: ExperimentKey[] | null | undefined): ExperimentKey[] | null | undefined {
|
|
90
|
+
if (experimentsInput === undefined) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
if (experimentsInput === null) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return experimentsInput.filter(exp => exp in experiments);
|
|
97
|
+
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { Link, Publication } from "@readium/shared";
|
|
2
|
-
import { webPubStylesheet } from "./css/WebPubStylesheet";
|
|
3
2
|
|
|
4
|
-
//
|
|
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
|
|
5
9
|
const blobify = (source: string, type: string) => URL.createObjectURL(new Blob([source], { type }));
|
|
6
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`);
|
|
7
14
|
const scriptify = (doc: Document, source: string) => {
|
|
8
15
|
const s = doc.createElement("script");
|
|
9
16
|
s.dataset.readium = "true";
|
|
@@ -11,16 +18,18 @@ const scriptify = (doc: Document, source: string) => {
|
|
|
11
18
|
return s;
|
|
12
19
|
}
|
|
13
20
|
const styleify = (doc: Document, source: string) => {
|
|
14
|
-
const s = doc.createElement("
|
|
21
|
+
const s = doc.createElement("link");
|
|
15
22
|
s.dataset.readium = "true";
|
|
16
|
-
s.
|
|
23
|
+
s.rel = "stylesheet";
|
|
24
|
+
s.type = "text/css";
|
|
25
|
+
s.href = source.startsWith("blob:") ? source : blobify(source, "text/css");
|
|
17
26
|
return s;
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
type CacheFunction = () => string;
|
|
21
30
|
const resourceBlobCache = new Map<string, string>();
|
|
22
31
|
const cached = (key: string, cacher: CacheFunction) => {
|
|
23
|
-
if
|
|
32
|
+
if(resourceBlobCache.has(key)) return resourceBlobCache.get(key)!;
|
|
24
33
|
const value = cacher();
|
|
25
34
|
resourceBlobCache.set(key, value);
|
|
26
35
|
return value;
|
|
@@ -103,9 +112,9 @@ export class WebPubBlobBuilder {
|
|
|
103
112
|
private finalizeDOM(doc: Document, base: string | undefined, mediaType: any, txt?: string, cssProperties?: { [key: string]: string }): string {
|
|
104
113
|
if(!doc) return "";
|
|
105
114
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
115
|
+
// ReadiumCSS WebPub
|
|
116
|
+
doc.head.appendChild(styleify(doc, cached("ReadiumCSS-webpub", () => blobify(stripCSS(readiumCSSWebPub), "text/css"))));
|
|
117
|
+
|
|
109
118
|
if (cssProperties) {
|
|
110
119
|
this.setProperties(cssProperties, doc);
|
|
111
120
|
}
|
|
@@ -8,12 +8,13 @@ import { WebPubFrameManager } from "./WebPubFrameManager";
|
|
|
8
8
|
|
|
9
9
|
import { ManagerEventKey } from "../epub/EpubNavigator";
|
|
10
10
|
import { WebPubCSS } from "./css/WebPubCSS";
|
|
11
|
-
import { WebUserProperties } from "./css/Properties";
|
|
11
|
+
import { WebUserProperties, WebRSProperties } from "./css/Properties";
|
|
12
12
|
import { IWebPubPreferences, WebPubPreferences } from "./preferences/WebPubPreferences";
|
|
13
13
|
import { IWebPubDefaults, WebPubDefaults } from "./preferences/WebPubDefaults";
|
|
14
14
|
import { WebPubSettings } from "./preferences/WebPubSettings";
|
|
15
15
|
import { IPreferencesEditor } from "../preferences/PreferencesEditor";
|
|
16
16
|
import { WebPubPreferencesEditor } from "./preferences/WebPubPreferencesEditor";
|
|
17
|
+
|
|
17
18
|
export interface WebPubNavigatorConfiguration {
|
|
18
19
|
preferences: IWebPubPreferences;
|
|
19
20
|
defaults: IWebPubDefaults;
|
|
@@ -74,6 +75,7 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
74
75
|
this._defaults = new WebPubDefaults(configuration.defaults);
|
|
75
76
|
this._settings = new WebPubSettings(this._preferences, this._defaults, this.hasDisplayTransformability);
|
|
76
77
|
this._css = new WebPubCSS({
|
|
78
|
+
rsProperties: new WebRSProperties({ experiments: this._settings.experiments || null }),
|
|
77
79
|
userProperties: new WebUserProperties({ zoom: this._settings.zoom })
|
|
78
80
|
});
|
|
79
81
|
|
|
@@ -135,6 +137,12 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
135
137
|
private compileCSSProperties(css: WebPubCSS) {
|
|
136
138
|
const properties: { [key: string]: string } = {};
|
|
137
139
|
|
|
140
|
+
// Include RS properties (i.e. experiments)
|
|
141
|
+
for (const [key, value] of Object.entries(css.rsProperties.toCSSProperties())) {
|
|
142
|
+
properties[key] = value;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Include user properties
|
|
138
146
|
for (const [key, value] of Object.entries(css.userProperties.toCSSProperties())) {
|
|
139
147
|
properties[key] = value;
|
|
140
148
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TextAlignment } from "../../preferences/Types";
|
|
1
|
+
import { ExperimentKey, experiments, TextAlignment } from "../../preferences/Types";
|
|
2
2
|
import { BodyHyphens, Ligatures, Properties } from "../../css/Properties";
|
|
3
3
|
|
|
4
4
|
export interface IWebUserProperties {
|
|
@@ -6,6 +6,8 @@ export interface IWebUserProperties {
|
|
|
6
6
|
bodyHyphens?: BodyHyphens | null;
|
|
7
7
|
fontFamily?: string | null;
|
|
8
8
|
fontWeight?: number | null;
|
|
9
|
+
iOSPatch?: boolean | null;
|
|
10
|
+
iPadOSPatch?: boolean | null;
|
|
9
11
|
letterSpacing?: number | null;
|
|
10
12
|
ligatures?: Ligatures | null;
|
|
11
13
|
lineHeight?: number | null;
|
|
@@ -22,6 +24,8 @@ export class WebUserProperties extends Properties {
|
|
|
22
24
|
bodyHyphens: BodyHyphens | null;
|
|
23
25
|
fontFamily: string | null;
|
|
24
26
|
fontWeight: number | null;
|
|
27
|
+
iOSPatch: boolean | null;
|
|
28
|
+
iPadOSPatch: boolean | null;
|
|
25
29
|
letterSpacing: number | null;
|
|
26
30
|
ligatures: Ligatures | null;
|
|
27
31
|
lineHeight: number | null;
|
|
@@ -38,6 +42,8 @@ export class WebUserProperties extends Properties {
|
|
|
38
42
|
this.bodyHyphens = props.bodyHyphens ?? null;
|
|
39
43
|
this.fontFamily = props.fontFamily ?? null;
|
|
40
44
|
this.fontWeight = props.fontWeight ?? null;
|
|
45
|
+
this.iOSPatch = props.iOSPatch ?? null;
|
|
46
|
+
this.iPadOSPatch = props.iPadOSPatch ?? null;
|
|
41
47
|
this.letterSpacing = props.letterSpacing ?? null;
|
|
42
48
|
this.ligatures = props.ligatures ?? null;
|
|
43
49
|
this.lineHeight = props.lineHeight ?? null;
|
|
@@ -56,6 +62,8 @@ export class WebUserProperties extends Properties {
|
|
|
56
62
|
if (this.bodyHyphens) cssProperties["--USER__bodyHyphens"] = this.bodyHyphens;
|
|
57
63
|
if (this.fontFamily) cssProperties["--USER__fontFamily"] = this.fontFamily;
|
|
58
64
|
if (this.fontWeight != null) cssProperties["--USER__fontWeight"] = this.toUnitless(this.fontWeight);
|
|
65
|
+
if (this.iOSPatch) cssProperties["--USER__iOSPatch"] = this.toFlag("iOSPatch");
|
|
66
|
+
if (this.iPadOSPatch) cssProperties["--USER__iPadOSPatch"] = this.toFlag("iPadOSPatch");
|
|
59
67
|
if (this.letterSpacing != null) cssProperties["--USER__letterSpacing"] = this.toRem(this.letterSpacing);
|
|
60
68
|
if (this.ligatures) cssProperties["--USER__ligatures"] = this.ligatures;
|
|
61
69
|
if (this.lineHeight != null) cssProperties["--USER__lineHeight"] = this.toUnitless(this.lineHeight);
|
|
@@ -69,3 +77,28 @@ export class WebUserProperties extends Properties {
|
|
|
69
77
|
return cssProperties;
|
|
70
78
|
}
|
|
71
79
|
}
|
|
80
|
+
|
|
81
|
+
export interface IWebRSProperties {
|
|
82
|
+
experiments: Array<ExperimentKey> | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class WebRSProperties extends Properties {
|
|
86
|
+
experiments: Array<ExperimentKey> | null;
|
|
87
|
+
|
|
88
|
+
constructor(props: IWebRSProperties) {
|
|
89
|
+
super();
|
|
90
|
+
this.experiments = props.experiments ?? null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
toCSSProperties() {
|
|
94
|
+
const cssProperties: { [key: string]: string } = {};
|
|
95
|
+
|
|
96
|
+
if (this.experiments) {
|
|
97
|
+
this.experiments.forEach((exp) => {
|
|
98
|
+
cssProperties["--RS__" + exp] = experiments[exp].value;
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return cssProperties;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { WebPubSettings } from "../preferences/WebPubSettings";
|
|
2
|
-
import { IWebUserProperties, WebUserProperties } from "./Properties";
|
|
2
|
+
import { IWebUserProperties, WebRSProperties, WebUserProperties } from "./Properties";
|
|
3
3
|
|
|
4
4
|
export interface IWebPubCSS {
|
|
5
|
+
rsProperties: WebRSProperties;
|
|
5
6
|
userProperties: WebUserProperties;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export class WebPubCSS {
|
|
10
|
+
rsProperties: WebRSProperties;
|
|
9
11
|
userProperties: WebUserProperties;
|
|
10
12
|
|
|
11
13
|
constructor(props: IWebPubCSS) {
|
|
14
|
+
this.rsProperties = props.rsProperties;
|
|
12
15
|
this.userProperties = props.userProperties;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
update(settings: WebPubSettings) {
|
|
19
|
+
if (settings.experiments) {
|
|
20
|
+
this.rsProperties.experiments = settings.experiments;
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
const updated: IWebUserProperties = {
|
|
17
24
|
a11yNormalize: settings.textNormalization,
|
|
18
25
|
bodyHyphens: typeof settings.hyphens !== "boolean"
|
|
@@ -22,6 +29,8 @@ export class WebPubCSS {
|
|
|
22
29
|
: "none",
|
|
23
30
|
fontFamily: settings.fontFamily,
|
|
24
31
|
fontWeight: settings.fontWeight,
|
|
32
|
+
iOSPatch: settings.iOSPatch,
|
|
33
|
+
iPadOSPatch: settings.iPadOSPatch,
|
|
25
34
|
letterSpacing: settings.letterSpacing,
|
|
26
35
|
ligatures: typeof settings.ligatures !== "boolean"
|
|
27
36
|
? null
|
package/src/webpub/css/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ExperimentKey,
|
|
2
3
|
fontWeightRangeConfig,
|
|
3
4
|
TextAlignment,
|
|
4
5
|
zoomRangeConfig
|
|
@@ -9,13 +10,18 @@ import {
|
|
|
9
10
|
ensureEnumValue,
|
|
10
11
|
ensureNonNegative,
|
|
11
12
|
ensureValueInRange,
|
|
12
|
-
ensureString
|
|
13
|
+
ensureString,
|
|
14
|
+
ensureExperiment
|
|
13
15
|
} from "../../preferences/guards";
|
|
14
16
|
|
|
17
|
+
import { sMLWithRequest } from "../../helpers";
|
|
18
|
+
|
|
15
19
|
export interface IWebPubDefaults {
|
|
16
20
|
fontFamily?: string | null,
|
|
17
21
|
fontWeight?: number | null,
|
|
18
22
|
hyphens?: boolean | null,
|
|
23
|
+
iOSPatch?: boolean | null,
|
|
24
|
+
iPadOSPatch?: boolean | null,
|
|
19
25
|
letterSpacing?: number | null,
|
|
20
26
|
ligatures?: boolean | null,
|
|
21
27
|
lineHeight?: number | null,
|
|
@@ -25,13 +31,16 @@ export interface IWebPubDefaults {
|
|
|
25
31
|
textAlign?: TextAlignment | null,
|
|
26
32
|
textNormalization?: boolean | null,
|
|
27
33
|
wordSpacing?: number | null,
|
|
28
|
-
zoom?: number | null
|
|
34
|
+
zoom?: number | null,
|
|
35
|
+
experiments?: Array<ExperimentKey> | null,
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export class WebPubDefaults {
|
|
32
39
|
fontFamily: string | null;
|
|
33
40
|
fontWeight: number | null;
|
|
34
41
|
hyphens: boolean | null;
|
|
42
|
+
iOSPatch: boolean | null;
|
|
43
|
+
iPadOSPatch: boolean | null;
|
|
35
44
|
letterSpacing: number | null;
|
|
36
45
|
ligatures: boolean | null;
|
|
37
46
|
lineHeight: number | null;
|
|
@@ -42,11 +51,18 @@ export class WebPubDefaults {
|
|
|
42
51
|
textNormalization: boolean | null;
|
|
43
52
|
wordSpacing: number | null;
|
|
44
53
|
zoom: number;
|
|
54
|
+
experiments: Array<ExperimentKey> | null;
|
|
45
55
|
|
|
46
56
|
constructor(defaults: IWebPubDefaults) {
|
|
47
57
|
this.fontFamily = ensureString(defaults.fontFamily) || null;
|
|
48
58
|
this.fontWeight = ensureValueInRange(defaults.fontWeight, fontWeightRangeConfig.range) || null;
|
|
49
59
|
this.hyphens = ensureBoolean(defaults.hyphens) ?? null;
|
|
60
|
+
this.iOSPatch = defaults.iOSPatch === false
|
|
61
|
+
? false
|
|
62
|
+
: ((sMLWithRequest.OS.iOS || sMLWithRequest.OS.iPadOS) && sMLWithRequest.iOSRequest === "mobile");
|
|
63
|
+
this.iPadOSPatch = defaults.iPadOSPatch === false
|
|
64
|
+
? false
|
|
65
|
+
: (sMLWithRequest.OS.iPadOS && sMLWithRequest.iOSRequest === "desktop");
|
|
50
66
|
this.letterSpacing = ensureNonNegative(defaults.letterSpacing) || null;
|
|
51
67
|
this.ligatures = ensureBoolean(defaults.ligatures) ?? null;
|
|
52
68
|
this.lineHeight = ensureNonNegative(defaults.lineHeight) || null;
|
|
@@ -57,5 +73,6 @@ export class WebPubDefaults {
|
|
|
57
73
|
this.textNormalization = ensureBoolean(defaults.textNormalization) ?? false;
|
|
58
74
|
this.wordSpacing = ensureNonNegative(defaults.wordSpacing) || null;
|
|
59
75
|
this.zoom = ensureValueInRange(defaults.zoom, zoomRangeConfig.range) || 1;
|
|
76
|
+
this.experiments = ensureExperiment(defaults.experiments) ?? null;
|
|
60
77
|
}
|
|
61
78
|
}
|
|
@@ -18,6 +18,8 @@ export interface IWebPubPreferences {
|
|
|
18
18
|
fontFamily?: string | null,
|
|
19
19
|
fontWeight?: number | null,
|
|
20
20
|
hyphens?: boolean | null,
|
|
21
|
+
iOSPatch?: boolean | null,
|
|
22
|
+
iPadOSPatch?: boolean | null,
|
|
21
23
|
letterSpacing?: number | null,
|
|
22
24
|
ligatures?: boolean | null,
|
|
23
25
|
lineHeight?: number | null,
|
|
@@ -34,6 +36,8 @@ export class WebPubPreferences implements ConfigurablePreferences {
|
|
|
34
36
|
fontFamily?: string | null;
|
|
35
37
|
fontWeight?: number | null;
|
|
36
38
|
hyphens?: boolean | null;
|
|
39
|
+
iOSPatch?: boolean | null;
|
|
40
|
+
iPadOSPatch?: boolean | null;
|
|
37
41
|
letterSpacing?: number | null;
|
|
38
42
|
ligatures?: boolean | null;
|
|
39
43
|
lineHeight?: number | null;
|
|
@@ -49,6 +53,8 @@ export class WebPubPreferences implements ConfigurablePreferences {
|
|
|
49
53
|
this.fontFamily = ensureString(preferences.fontFamily);
|
|
50
54
|
this.fontWeight = ensureValueInRange(preferences.fontWeight, fontWeightRangeConfig.range);
|
|
51
55
|
this.hyphens = ensureBoolean(preferences.hyphens);
|
|
56
|
+
this.iOSPatch = ensureBoolean(preferences.iOSPatch);
|
|
57
|
+
this.iPadOSPatch = ensureBoolean(preferences.iPadOSPatch);
|
|
52
58
|
this.letterSpacing = ensureNonNegative(preferences.letterSpacing);
|
|
53
59
|
this.ligatures = ensureBoolean(preferences.ligatures);
|
|
54
60
|
this.lineHeight = ensureNonNegative(preferences.lineHeight);
|