@readium/navigator 2.1.1 → 2.2.1

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 (52) hide show
  1. package/dist/index.js +3912 -2721
  2. package/dist/index.umd.cjs +286 -71
  3. package/package.json +1 -1
  4. package/src/css/Properties.ts +47 -0
  5. package/src/css/index.ts +1 -0
  6. package/src/epub/css/Properties.ts +10 -48
  7. package/src/epub/preferences/EpubDefaults.ts +1 -1
  8. package/src/epub/preferences/EpubPreferences.ts +1 -1
  9. package/src/epub/preferences/EpubPreferencesEditor.ts +30 -23
  10. package/src/index.ts +3 -1
  11. package/src/preferences/Types.ts +40 -0
  12. package/src/{epub/preferences → preferences}/guards.ts +5 -6
  13. package/src/preferences/index.ts +2 -1
  14. package/src/webpub/WebPubBlobBuilder.ts +167 -0
  15. package/src/webpub/WebPubFrameManager.ts +156 -0
  16. package/src/webpub/WebPubFramePoolManager.ts +221 -0
  17. package/src/webpub/WebPubNavigator.ts +494 -0
  18. package/src/webpub/css/Properties.ts +71 -0
  19. package/src/webpub/css/WebPubCSS.ts +42 -0
  20. package/src/webpub/css/WebPubStylesheet.ts +204 -0
  21. package/src/webpub/css/index.ts +3 -0
  22. package/src/webpub/index.ts +6 -0
  23. package/src/webpub/preferences/WebPubDefaults.ts +61 -0
  24. package/src/webpub/preferences/WebPubPreferences.ts +88 -0
  25. package/src/webpub/preferences/WebPubPreferencesEditor.ts +193 -0
  26. package/src/webpub/preferences/WebPubSettings.ts +88 -0
  27. package/src/webpub/preferences/index.ts +4 -0
  28. package/types/src/css/Properties.d.ts +20 -0
  29. package/types/src/css/index.d.ts +1 -0
  30. package/types/src/epub/css/Properties.d.ts +1 -21
  31. package/types/src/index.d.ts +2 -0
  32. package/types/src/preferences/Types.d.ts +8 -0
  33. package/types/src/preferences/guards.d.ts +9 -0
  34. package/types/src/preferences/index.d.ts +1 -0
  35. package/types/src/web/WebPubBlobBuilder.d.ts +10 -0
  36. package/types/src/web/WebPubFrameManager.d.ts +20 -0
  37. package/types/src/web/WebPubNavigator.d.ts +48 -0
  38. package/types/src/web/index.d.ts +3 -0
  39. package/types/src/webpub/WebPubBlobBuilder.d.ts +16 -0
  40. package/types/src/webpub/WebPubFrameManager.d.ts +24 -0
  41. package/types/src/webpub/WebPubFramePoolManager.d.ts +23 -0
  42. package/types/src/webpub/WebPubNavigator.d.ts +70 -0
  43. package/types/src/webpub/css/Properties.d.ts +36 -0
  44. package/types/src/webpub/css/WebPubCSS.d.ts +10 -0
  45. package/types/src/webpub/css/WebPubStylesheet.d.ts +1 -0
  46. package/types/src/webpub/css/index.d.ts +3 -0
  47. package/types/src/webpub/index.d.ts +6 -0
  48. package/types/src/webpub/preferences/WebPubDefaults.d.ts +32 -0
  49. package/types/src/webpub/preferences/WebPubPreferences.d.ts +36 -0
  50. package/types/src/webpub/preferences/WebPubPreferencesEditor.d.ts +27 -0
  51. package/types/src/webpub/preferences/WebPubSettings.d.ts +35 -0
  52. package/types/src/webpub/preferences/index.d.ts +4 -0
@@ -0,0 +1,494 @@
1
+ import { Link, Locator, Publication, ReadingProgression, LocatorLocations } from "@readium/shared";
2
+ import { VisualNavigator, VisualNavigatorViewport, ProgressionRange } from "../Navigator";
3
+ import { Configurable } from "../preferences/Configurable";
4
+ import { WebPubFramePoolManager } from "./WebPubFramePoolManager";
5
+ import { BasicTextSelection, CommsEventKey, FrameClickEvent, ModuleLibrary, ModuleName, WebPubModules } from "@readium/navigator-html-injectables";
6
+ import * as path from "path-browserify";
7
+ import { ManagerEventKey } from "../epub/EpubNavigator";
8
+ import { WebPubCSS } from "./css/WebPubCSS";
9
+ import { WebUserProperties } from "./css/Properties";
10
+ import { IWebPubPreferences, WebPubPreferences } from "./preferences/WebPubPreferences";
11
+ import { IWebPubDefaults, WebPubDefaults } from "./preferences/WebPubDefaults";
12
+ import { WebPubSettings } from "./preferences/WebPubSettings";
13
+ import { IPreferencesEditor } from "../preferences/PreferencesEditor";
14
+ import { WebPubPreferencesEditor } from "./preferences/WebPubPreferencesEditor";
15
+
16
+ export interface WebPubNavigatorConfiguration {
17
+ preferences: IWebPubPreferences;
18
+ defaults: IWebPubDefaults;
19
+ }
20
+
21
+ export interface WebPubNavigatorListeners {
22
+ frameLoaded: (wnd: Window) => void;
23
+ positionChanged: (locator: Locator) => void;
24
+ tap: (e: FrameClickEvent) => boolean;
25
+ click: (e: FrameClickEvent) => boolean;
26
+ zoom: (scale: number) => void;
27
+ scroll: (delta: number) => void;
28
+ customEvent: (key: string, data: unknown) => void;
29
+ handleLocator: (locator: Locator) => boolean;
30
+ textSelected: (selection: BasicTextSelection) => void;
31
+ }
32
+
33
+ const defaultListeners = (listeners: WebPubNavigatorListeners): WebPubNavigatorListeners => ({
34
+ frameLoaded: listeners.frameLoaded || (() => {}),
35
+ positionChanged: listeners.positionChanged || (() => {}),
36
+ tap: listeners.tap || (() => false),
37
+ click: listeners.click || (() => false),
38
+ zoom: listeners.zoom || (() => {}),
39
+ scroll: listeners.scroll || (() => {}),
40
+ customEvent: listeners.customEvent || (() => {}),
41
+ handleLocator: listeners.handleLocator || (() => false),
42
+ textSelected: listeners.textSelected || (() => {})
43
+ })
44
+
45
+ export class WebPubNavigator extends VisualNavigator implements Configurable<WebPubSettings, WebPubPreferences> {
46
+ private readonly pub: Publication;
47
+ private readonly container: HTMLElement;
48
+ private readonly listeners: WebPubNavigatorListeners;
49
+ private framePool!: WebPubFramePoolManager;
50
+ private currentIndex: number = 0;
51
+ private currentLocation: Locator;
52
+
53
+ private _preferences: WebPubPreferences;
54
+ private _defaults: WebPubDefaults;
55
+ private _settings: WebPubSettings;
56
+ private _css: WebPubCSS;
57
+ private _preferencesEditor: WebPubPreferencesEditor | null = null;
58
+
59
+ private webViewport: VisualNavigatorViewport = {
60
+ readingOrder: [],
61
+ progressions: new Map(),
62
+ positions: null
63
+ };
64
+
65
+ constructor(container: HTMLElement, pub: Publication, listeners: WebPubNavigatorListeners, initialPosition: Locator | undefined = undefined, configuration: WebPubNavigatorConfiguration = { preferences: {}, defaults: {} }) {
66
+ super();
67
+ this.pub = pub;
68
+ this.container = container;
69
+ this.listeners = defaultListeners(listeners);
70
+
71
+ // Initialize preference system
72
+ this._preferences = new WebPubPreferences(configuration.preferences);
73
+ this._defaults = new WebPubDefaults(configuration.defaults);
74
+ this._settings = new WebPubSettings(this._preferences, this._defaults);
75
+ this._css = new WebPubCSS({
76
+ userProperties: new WebUserProperties({ zoom: this._settings.zoom })
77
+ });
78
+
79
+ // Initialize current location
80
+ if (initialPosition && typeof initialPosition.copyWithLocations === 'function') {
81
+ this.currentLocation = initialPosition;
82
+ // Update currentIndex to match the initial position
83
+ const index = this.pub.readingOrder.findIndexWithHref(initialPosition.href);
84
+ if (index >= 0) {
85
+ this.currentIndex = index;
86
+ }
87
+ } else {
88
+ this.currentLocation = this.createCurrentLocator();
89
+ }
90
+ }
91
+
92
+ public async load() {
93
+ await this.updateCSS(false);
94
+ const cssProperties = this.compileCSSProperties(this._css);
95
+ this.framePool = new WebPubFramePoolManager(this.container, cssProperties);
96
+
97
+ await this.apply();
98
+ }
99
+
100
+ // Configurable interface implementation
101
+ public get settings(): Readonly<WebPubSettings> {
102
+ return Object.freeze({ ...this._settings });
103
+ }
104
+
105
+ public get preferencesEditor(): IPreferencesEditor {
106
+ if (this._preferencesEditor === null) {
107
+ this._preferencesEditor = new WebPubPreferencesEditor(this._preferences, this.settings, this.pub.metadata);
108
+ }
109
+ return this._preferencesEditor;
110
+ }
111
+
112
+ public async submitPreferences(preferences: WebPubPreferences) {
113
+ this._preferences = this._preferences.merging(preferences) as WebPubPreferences;
114
+ await this.applyPreferences();
115
+ }
116
+
117
+ private async applyPreferences() {
118
+ this._settings = new WebPubSettings(this._preferences, this._defaults);
119
+
120
+ if (this._preferencesEditor !== null) {
121
+ this._preferencesEditor = new WebPubPreferencesEditor(this._preferences, this.settings, this.pub.metadata);
122
+ }
123
+
124
+ // Apply preferences using CSS system like EPUB
125
+ await this.updateCSS(true);
126
+ }
127
+
128
+ private async updateCSS(commit: boolean) {
129
+ this._css.update(this._settings);
130
+
131
+ if (commit) await this.commitCSS(this._css);
132
+ };
133
+
134
+ private compileCSSProperties(css: WebPubCSS) {
135
+ const properties: { [key: string]: string } = {};
136
+
137
+ for (const [key, value] of Object.entries(css.userProperties.toCSSProperties())) {
138
+ properties[key] = value;
139
+ }
140
+
141
+ return properties;
142
+ }
143
+
144
+ private async commitCSS(css: WebPubCSS) {
145
+ const properties = this.compileCSSProperties(css);
146
+ this.framePool.setCSSProperties(properties);
147
+ }
148
+
149
+ public eventListener(key: CommsEventKey | ManagerEventKey, data: unknown) {
150
+ switch (key) {
151
+ case "_pong":
152
+ this.listeners.frameLoaded(this.framePool.currentFrames[0]!.iframe.contentWindow!);
153
+ this.listeners.positionChanged(this.currentLocation);
154
+ break;
155
+ case "first_visible_locator":
156
+ const loc = Locator.deserialize(data as string);
157
+ if(!loc) break;
158
+ this.currentLocation = new Locator({
159
+ href: this.currentLocation.href,
160
+ type: this.currentLocation.type,
161
+ title: this.currentLocation.title,
162
+ locations: loc?.locations,
163
+ text: loc?.text
164
+ });
165
+ this.listeners.positionChanged(this.currentLocation);
166
+ break;
167
+ case "text_selected":
168
+ this.listeners.textSelected(data as BasicTextSelection);
169
+ break;
170
+ case "click":
171
+ case "tap":
172
+ const edata = data as FrameClickEvent;
173
+ if (edata.interactiveElement) {
174
+ const element = new DOMParser().parseFromString(
175
+ edata.interactiveElement,
176
+ "text/html"
177
+ ).body.children[0];
178
+ if (
179
+ element.nodeType === element.ELEMENT_NODE &&
180
+ element.nodeName === "A" &&
181
+ element.hasAttribute("href")
182
+ ) {
183
+ const origHref = element.attributes.getNamedItem("href")?.value!;
184
+ if (origHref.startsWith("#")) {
185
+ this.go(this.currentLocation.copyWithLocations({
186
+ fragments: [origHref.substring(1)]
187
+ }), false, () => { });
188
+ } else if(
189
+ origHref.startsWith("mailto:") ||
190
+ origHref.startsWith("tel:")
191
+ ) {
192
+ this.listeners.handleLocator(new Link({
193
+ href: origHref,
194
+ }).locator);
195
+ } else {
196
+ // Handle internal links that should navigate within the WebPub
197
+ // This includes relative links and full URLs that might be in the readingOrder
198
+ try {
199
+ let hrefToCheck;
200
+
201
+ // If origHref is already a full URL, use it directly
202
+ if (origHref.startsWith("http://") || origHref.startsWith("https://")) {
203
+ hrefToCheck = origHref;
204
+ } else {
205
+ // For relative URLs, use different strategies based on base URL format
206
+ if (this.currentLocation.href.startsWith("http://") || this.currentLocation.href.startsWith("https://")) {
207
+ // Base URL is absolute, use URL constructor
208
+ const currentUrl = new URL(this.currentLocation.href);
209
+ const resolvedUrl = new URL(origHref, currentUrl);
210
+ hrefToCheck = resolvedUrl.href;
211
+ } else {
212
+ // Base URL is relative, use path operations
213
+ hrefToCheck = path.join(path.dirname(this.currentLocation.href), origHref);
214
+ }
215
+ }
216
+
217
+ const link = this.pub.readingOrder.findWithHref(hrefToCheck);
218
+ if (link) {
219
+ this.goLink(link, false, () => { });
220
+ } else {
221
+ console.warn(`Internal link not found in readingOrder: ${hrefToCheck}`);
222
+ this.listeners.handleLocator(new Link({
223
+ href: origHref,
224
+ }).locator);
225
+ }
226
+ } catch (error) {
227
+ console.warn(`Couldn't resolve internal link for ${origHref}: ${error}`);
228
+ this.listeners.handleLocator(new Link({
229
+ href: origHref,
230
+ }).locator);
231
+ }
232
+ }
233
+ } else console.log("Clicked on", element);
234
+ } else {
235
+ const handled = key === "click" ? this.listeners.click(edata) : this.listeners.tap(edata);
236
+ if(handled) break;
237
+ }
238
+ break;
239
+ case "scroll":
240
+ this.listeners.scroll(data as number);
241
+ break;
242
+ case "zoom":
243
+ this.listeners.zoom(data as number);
244
+ break;
245
+ case "progress":
246
+ this.syncLocation(data as ProgressionRange);
247
+ break;
248
+ case "log":
249
+ console.log(this.framePool.currentFrames[0]?.source?.split("/")[3], ...(data as any[]));
250
+ break;
251
+ default:
252
+ this.listeners.customEvent(key, data);
253
+ break;
254
+ }
255
+ }
256
+
257
+ private determineModules(): ModuleName[] {
258
+ let modules = Array.from(ModuleLibrary.keys()) as ModuleName[];
259
+
260
+ // For WebPub, use the predefined WebPubModules array and filter
261
+ return modules.filter((m) => WebPubModules.includes(m));
262
+ }
263
+
264
+ private attachListener() {
265
+ if (this.framePool.currentFrames[0]?.msg) {
266
+ this.framePool.currentFrames[0].msg.listener = (key: CommsEventKey | ManagerEventKey, value: unknown) => {
267
+ this.eventListener(key, value);
268
+ };
269
+ }
270
+ }
271
+
272
+ private async apply() {
273
+ await this.framePool.update(this.pub, this.currentLocation, this.determineModules());
274
+
275
+ this.attachListener();
276
+
277
+ const idx = this.pub.readingOrder.findIndexWithHref(this.currentLocation.href);
278
+ if (idx < 0)
279
+ throw Error("Link for " + this.currentLocation.href + " not found!");
280
+ }
281
+
282
+ public async destroy() {
283
+ await this.framePool?.destroy();
284
+ }
285
+
286
+ private async changeResource(relative: number): Promise<boolean> {
287
+ if (relative === 0) return false;
288
+
289
+ const curr = this.pub.readingOrder.findIndexWithHref(this.currentLocation.href);
290
+ const i = Math.max(
291
+ 0,
292
+ Math.min(this.pub.readingOrder.items.length - 1, curr + relative)
293
+ );
294
+ if (i === curr) {
295
+ return false;
296
+ }
297
+ this.currentIndex = i;
298
+ this.currentLocation = this.createCurrentLocator();
299
+ await this.apply();
300
+ return true;
301
+ }
302
+
303
+ private updateViewport(progression: ProgressionRange) {
304
+ this.webViewport.readingOrder = [];
305
+ this.webViewport.progressions.clear();
306
+ this.webViewport.positions = null;
307
+
308
+ // Use the current position's href
309
+ if (this.currentLocation) {
310
+ this.webViewport.readingOrder.push(this.currentLocation.href);
311
+ this.webViewport.progressions.set(this.currentLocation.href, progression);
312
+
313
+ if (this.currentLocation.locations?.position !== undefined) {
314
+ this.webViewport.positions = [this.currentLocation.locations.position];
315
+ // WebPub doesn't have lastLocationInView like EPUB, so no second position
316
+ }
317
+ }
318
+ }
319
+
320
+ private async syncLocation(iframeProgress: ProgressionRange): Promise<void> {
321
+ const progression = iframeProgress;
322
+ if (this.currentLocation) {
323
+ this.currentLocation = this.currentLocation.copyWithLocations({
324
+ progression: progression.start
325
+ });
326
+ }
327
+
328
+ this.updateViewport(progression);
329
+ this.listeners.positionChanged(this.currentLocation);
330
+ await this.framePool.update(this.pub, this.currentLocation, this.determineModules());
331
+ }
332
+
333
+ goBackward(_animated: boolean, cb: (ok: boolean) => void): void {
334
+ this.changeResource(-1).then((success) => {
335
+ cb(success);
336
+ });
337
+ }
338
+
339
+ goForward(_animated: boolean, cb: (ok: boolean) => void): void {
340
+ this.changeResource(1).then((success) => {
341
+ cb(success);
342
+ });
343
+ }
344
+
345
+ get currentLocator(): Locator {
346
+ return this.currentLocation;
347
+ }
348
+
349
+ get viewport(): VisualNavigatorViewport {
350
+ return this.webViewport;
351
+ }
352
+
353
+ get isScrollStart(): boolean {
354
+ const firstHref = this.viewport.readingOrder[0];
355
+ const progression = this.viewport.progressions.get(firstHref);
356
+ return progression?.start === 0;
357
+ }
358
+
359
+ get isScrollEnd(): boolean {
360
+ const lastHref = this.viewport.readingOrder[this.viewport.readingOrder.length - 1];
361
+ const progression = this.viewport.progressions.get(lastHref);
362
+ return progression?.end === 1;
363
+ }
364
+
365
+ get canGoBackward(): boolean {
366
+ const firstResource = this.pub.readingOrder.items[0]?.href;
367
+ return !(this.viewport.progressions.has(firstResource) && this.viewport.progressions.get(firstResource)?.start === 0);
368
+ }
369
+
370
+ get canGoForward(): boolean {
371
+ const lastResource = this.pub.readingOrder.items[this.pub.readingOrder.items.length - 1]?.href;
372
+ return !(this.viewport.progressions.has(lastResource) && this.viewport.progressions.get(lastResource)?.end === 1);
373
+ }
374
+
375
+ get readingProgression(): ReadingProgression {
376
+ return this.pub.metadata.effectiveReadingProgression;
377
+ }
378
+
379
+ get publication(): Publication {
380
+ return this.pub;
381
+ }
382
+
383
+ private async loadLocator(locator: Locator, cb: (ok: boolean) => void) {
384
+ let done = false;
385
+ let cssSelector = (typeof locator.locations.getCssSelector === 'function') && locator.locations.getCssSelector();
386
+ if(locator.text?.highlight) {
387
+ done = await new Promise<boolean>((res, _) => {
388
+ // Attempt to go to a highlighted piece of text in the resource
389
+ this.framePool.currentFrames[0]!.msg!.send(
390
+ "go_text",
391
+ cssSelector ? [
392
+ locator.text?.serialize(),
393
+ cssSelector // Include CSS selector if it exists
394
+ ] : locator.text?.serialize(),
395
+ (ok) => res(ok)
396
+ );
397
+ });
398
+ } else if(cssSelector) {
399
+ done = await new Promise<boolean>((res, _) => {
400
+ this.framePool.currentFrames[0]!.msg!.send(
401
+ "go_text",
402
+ [
403
+ "", // No text!
404
+ cssSelector // Just CSS selector
405
+ ],
406
+ (ok) => res(ok)
407
+ );
408
+ });
409
+ }
410
+ if(done) {
411
+ cb(done);
412
+ return;
413
+ }
414
+ // This sanity check has to be performed because we're still passing non-locator class
415
+ // locator objects to this function. This is not good and should eventually be forbidden
416
+ // or the locator should be deserialized sometime before this function.
417
+ const hid = (typeof locator.locations.htmlId === 'function') && locator.locations.htmlId();
418
+ if(hid)
419
+ done = await new Promise<boolean>((res, _) => {
420
+ // Attempt to go to an HTML ID in the resource
421
+ this.framePool.currentFrames[0]!.msg!.send("go_id", hid, (ok) => res(ok));
422
+ });
423
+ if(done) {
424
+ cb(done);
425
+ return;
426
+ }
427
+
428
+ const progression = locator?.locations?.progression;
429
+ const hasProgression = progression && progression > 0;
430
+ if(hasProgression)
431
+ done = await new Promise<boolean>((res, _) => {
432
+ // Attempt to go to a progression in the resource
433
+ this.framePool.currentFrames[0]!.msg!.send("go_progression", progression, (ok) => res(ok));
434
+ });
435
+ else done = true;
436
+ cb(done);
437
+ }
438
+
439
+ public go(locator: Locator, _: boolean, cb: (ok: boolean) => void): void {
440
+ const href = locator.href.split("#")[0];
441
+ let link = this.pub.readingOrder.findWithHref(href);
442
+ if(!link) {
443
+ return cb(this.listeners.handleLocator(locator));
444
+ }
445
+
446
+ // Update currentIndex to point to the found link
447
+ const index = this.pub.readingOrder.findIndexWithHref(href);
448
+ if (index >= 0) {
449
+ this.currentIndex = index;
450
+ }
451
+
452
+ this.currentLocation = this.createCurrentLocator();
453
+ this.apply().then(() => this.loadLocator(locator, (ok) => cb(ok))).then(() => {
454
+ // Now that we've gone to the right locator, we can attach the listeners.
455
+ // Doing this only at this stage reduces janky UI with multiple locator updates.
456
+ this.attachListener();
457
+ });
458
+ }
459
+
460
+ public goLink(link: Link, animated: boolean, cb: (ok: boolean) => void): void {
461
+ return this.go(link.locator, animated, cb);
462
+ }
463
+
464
+ // Specifics to WebPub
465
+ // Util method
466
+ private createCurrentLocator(): Locator {
467
+ const readingOrder = this.pub.readingOrder;
468
+ const currentLink = readingOrder.items[this.currentIndex];
469
+
470
+ if (!currentLink) {
471
+ throw new Error("No current resource available");
472
+ }
473
+
474
+ // Check if we're on the same resource
475
+ const isSameResource = this.currentLocation && this.currentLocation.href === currentLink.href;
476
+
477
+ // Preserve progression if staying on same resource, otherwise start from beginning
478
+ const progression = isSameResource && this.currentLocation.locations.progression
479
+ ? this.currentLocation.locations.progression
480
+ : 0;
481
+
482
+ return this.pub.manifest.locatorFromLink(currentLink) || new Locator({
483
+ href: currentLink.href,
484
+ type: currentLink.type || "text/html",
485
+ locations: new LocatorLocations({
486
+ fragments: [],
487
+ progression: progression,
488
+ position: this.currentIndex + 1
489
+ })
490
+ });
491
+ }
492
+ }
493
+
494
+ export const ExperimentalWebPubNavigator = WebPubNavigator;
@@ -0,0 +1,71 @@
1
+ import { TextAlignment } from "../../preferences/Types";
2
+ import { BodyHyphens, Ligatures, Properties } from "../../css/Properties";
3
+
4
+ export interface IWebUserProperties {
5
+ a11yNormalize?: boolean | null;
6
+ bodyHyphens?: BodyHyphens | null;
7
+ fontFamily?: string | null;
8
+ fontWeight?: number | null;
9
+ letterSpacing?: number | null;
10
+ ligatures?: Ligatures | null;
11
+ lineHeight?: number | null;
12
+ noRuby?: boolean | null;
13
+ paraIndent?: number | null;
14
+ paraSpacing?: number | null;
15
+ textAlign?: TextAlignment | null;
16
+ wordSpacing?: number | null;
17
+ zoom: number | null;
18
+ }
19
+
20
+ export class WebUserProperties extends Properties {
21
+ a11yNormalize: boolean | null;
22
+ bodyHyphens: BodyHyphens | null;
23
+ fontFamily: string | null;
24
+ fontWeight: number | null;
25
+ letterSpacing: number | null;
26
+ ligatures: Ligatures | null;
27
+ lineHeight: number | null;
28
+ noRuby: boolean | null;
29
+ paraIndent: number | null;
30
+ paraSpacing: number | null;
31
+ textAlign: TextAlignment | null;
32
+ wordSpacing: number | null;
33
+ zoom: number | null;
34
+
35
+ constructor(props: IWebUserProperties) {
36
+ super();
37
+ this.a11yNormalize = props.a11yNormalize ?? null;
38
+ this.bodyHyphens = props.bodyHyphens ?? null;
39
+ this.fontFamily = props.fontFamily ?? null;
40
+ this.fontWeight = props.fontWeight ?? null;
41
+ this.letterSpacing = props.letterSpacing ?? null;
42
+ this.ligatures = props.ligatures ?? null;
43
+ this.lineHeight = props.lineHeight ?? null;
44
+ this.noRuby = props.noRuby ?? null;
45
+ this.paraIndent = props.paraIndent ?? null;
46
+ this.paraSpacing = props.paraSpacing ?? null;
47
+ this.textAlign = props.textAlign ?? null;
48
+ this.wordSpacing = props.wordSpacing ?? null;
49
+ this.zoom = props.zoom ?? null;
50
+ }
51
+
52
+ toCSSProperties() {
53
+ const cssProperties: { [key: string]: string } = {};
54
+
55
+ if (this.a11yNormalize) cssProperties["--USER__a11yNormalize"] = this.toFlag("a11y");
56
+ if (this.bodyHyphens) cssProperties["--USER__bodyHyphens"] = this.bodyHyphens;
57
+ if (this.fontFamily) cssProperties["--USER__fontFamily"] = this.fontFamily;
58
+ if (this.fontWeight != null) cssProperties["--USER__fontWeight"] = this.toUnitless(this.fontWeight);
59
+ if (this.letterSpacing != null) cssProperties["--USER__letterSpacing"] = this.toRem(this.letterSpacing);
60
+ if (this.ligatures) cssProperties["--USER__ligatures"] = this.ligatures;
61
+ if (this.lineHeight != null) cssProperties["--USER__lineHeight"] = this.toUnitless(this.lineHeight);
62
+ if (this.noRuby) cssProperties["--USER__noRuby"] = this.toFlag("noRuby");
63
+ if (this.paraIndent != null) cssProperties["--USER__paraIndent"] = this.toRem(this.paraIndent);
64
+ if (this.paraSpacing != null) cssProperties["--USER__paraSpacing"] = this.toRem(this.paraSpacing);
65
+ if (this.textAlign) cssProperties["--USER__textAlign"] = this.textAlign;
66
+ if (this.wordSpacing != null) cssProperties["--USER__wordSpacing"] = this.toRem(this.wordSpacing);
67
+ if (this.zoom !== null) cssProperties["--USER__zoom"] = this.toPercentage(this.zoom, true);
68
+
69
+ return cssProperties;
70
+ }
71
+ }
@@ -0,0 +1,42 @@
1
+ import { WebPubSettings } from "../preferences/WebPubSettings";
2
+ import { IWebUserProperties, WebUserProperties } from "./Properties";
3
+
4
+ export interface IWebPubCSS {
5
+ userProperties: WebUserProperties;
6
+ }
7
+
8
+ export class WebPubCSS {
9
+ userProperties: WebUserProperties;
10
+
11
+ constructor(props: IWebPubCSS) {
12
+ this.userProperties = props.userProperties;
13
+ }
14
+
15
+ update(settings: WebPubSettings) {
16
+ const updated: IWebUserProperties = {
17
+ a11yNormalize: settings.textNormalization,
18
+ bodyHyphens: typeof settings.hyphens !== "boolean"
19
+ ? null
20
+ : settings.hyphens
21
+ ? "auto"
22
+ : "none",
23
+ fontFamily: settings.fontFamily,
24
+ fontWeight: settings.fontWeight,
25
+ letterSpacing: settings.letterSpacing,
26
+ ligatures: typeof settings.ligatures !== "boolean"
27
+ ? null
28
+ : settings.ligatures
29
+ ? "common-ligatures"
30
+ : "none",
31
+ lineHeight: settings.lineHeight,
32
+ noRuby: settings.noRuby,
33
+ paraIndent: settings.paragraphIndent,
34
+ paraSpacing: settings.paragraphSpacing,
35
+ textAlign: settings.textAlign,
36
+ wordSpacing: settings.wordSpacing,
37
+ zoom: settings.zoom
38
+ };
39
+
40
+ this.userProperties = new WebUserProperties(updated);
41
+ }
42
+ }