@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.
Files changed (35) hide show
  1. package/dist/index.js +1565 -1661
  2. package/dist/index.umd.cjs +20 -266
  3. package/package.json +4 -4
  4. package/src/epub/css/Properties.ts +10 -1
  5. package/src/epub/css/ReadiumCSS.ts +3 -0
  6. package/src/epub/frame/FrameBlobBuilder.ts +39 -11
  7. package/src/epub/frame/FrameManager.ts +1 -0
  8. package/src/epub/fxl/FXLFrameManager.ts +1 -0
  9. package/src/epub/preferences/EpubDefaults.ts +8 -2
  10. package/src/epub/preferences/EpubPreferencesEditor.ts +24 -3
  11. package/src/epub/preferences/EpubSettings.ts +6 -2
  12. package/src/preferences/Types.ts +6 -0
  13. package/src/preferences/guards.ts +12 -0
  14. package/src/webpub/WebPubBlobBuilder.ts +17 -8
  15. package/src/webpub/WebPubNavigator.ts +9 -1
  16. package/src/webpub/css/Properties.ts +34 -1
  17. package/src/webpub/css/WebPubCSS.ts +10 -1
  18. package/src/webpub/css/index.ts +1 -2
  19. package/src/webpub/preferences/WebPubDefaults.ts +19 -2
  20. package/src/webpub/preferences/WebPubPreferences.ts +6 -0
  21. package/src/webpub/preferences/WebPubPreferencesEditor.ts +22 -0
  22. package/src/webpub/preferences/WebPubSettings.ts +23 -2
  23. package/types/src/epub/css/Properties.d.ts +3 -1
  24. package/types/src/epub/preferences/EpubDefaults.d.ts +3 -1
  25. package/types/src/epub/preferences/EpubSettings.d.ts +3 -1
  26. package/types/src/preferences/Types.d.ts +14 -0
  27. package/types/src/preferences/guards.d.ts +2 -0
  28. package/types/src/webpub/css/Properties.d.ts +15 -1
  29. package/types/src/webpub/css/WebPubCSS.d.ts +3 -1
  30. package/types/src/webpub/css/index.d.ts +0 -1
  31. package/types/src/webpub/preferences/WebPubDefaults.d.ts +7 -1
  32. package/types/src/webpub/preferences/WebPubPreferences.d.ts +4 -0
  33. package/types/src/webpub/preferences/WebPubPreferencesEditor.d.ts +2 -0
  34. package/types/src/webpub/preferences/WebPubSettings.d.ts +7 -1
  35. 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",
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.20",
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.2",
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.1.5"
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
- type: mediaType.isHTML
256
- ? mediaType.string
257
- : "application/xhtml+xml", // Fallback to XHTML
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: this.layout !== Layout.fixed
260
- && this.metadata?.languages?.some(lang => lang === "ar" || lang === "fa")
261
- && this.preferences.ligatures !== null || false,
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
  }
@@ -1,3 +1,9 @@
1
+ import RCSSExperiments from "@readium/css/css/vars/experiments.json";
2
+
3
+ export type ExperimentKey = keyof typeof RCSSExperiments;
4
+
5
+ export const experiments = RCSSExperiments;
6
+
1
7
  export enum TextAlignment {
2
8
  start = "start",
3
9
  left = "left",
@@ -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
- // Utilities (matching FrameBlobBuilder pattern)
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("style");
21
+ const s = doc.createElement("link");
15
22
  s.dataset.readium = "true";
16
- s.textContent = source;
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 (resourceBlobCache.has(key)) return resourceBlobCache.get(key)!;
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
- // Add WebPubCSS stylesheet at end of head (like EPUB ReadiumCSS-after)
107
- const webPubStyle = styleify(doc, webPubStylesheet);
108
- doc.head.appendChild(webPubStyle);
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
@@ -1,3 +1,2 @@
1
1
  export * from "./Properties";
2
- export * from "./WebPubCSS";
3
- export * from "./WebPubStylesheet";
2
+ export * from "./WebPubCSS";
@@ -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);