@readium/navigator 1.3.4 → 2.0.0-beta.2

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 (55) hide show
  1. package/dist/index.js +3533 -2595
  2. package/dist/index.umd.cjs +16 -16
  3. package/package.json +9 -9
  4. package/src/epub/EpubNavigator.ts +184 -7
  5. package/src/epub/css/Properties.ts +376 -0
  6. package/src/epub/css/ReadiumCSS.ts +348 -0
  7. package/src/epub/css/index.ts +2 -0
  8. package/src/epub/frame/FrameBlobBuilder.ts +59 -9
  9. package/src/epub/frame/FrameManager.ts +16 -0
  10. package/src/epub/frame/FramePoolManager.ts +61 -2
  11. package/src/epub/fxl/FXLFramePoolManager.ts +3 -15
  12. package/src/epub/index.ts +3 -1
  13. package/src/epub/preferences/EpubDefaults.ts +154 -0
  14. package/src/epub/preferences/EpubPreferences.ts +183 -0
  15. package/src/epub/preferences/EpubPreferencesEditor.ts +501 -0
  16. package/src/epub/preferences/EpubSettings.ts +212 -0
  17. package/src/epub/preferences/guards.ts +86 -0
  18. package/src/epub/preferences/index.ts +4 -0
  19. package/src/helpers/dimensions.ts +13 -0
  20. package/src/helpers/index.ts +1 -0
  21. package/src/helpers/lineLength.ts +293 -0
  22. package/src/helpers/sML.ts +18 -1
  23. package/src/index.ts +2 -1
  24. package/src/preferences/Configurable.ts +16 -0
  25. package/src/preferences/Preference.ts +272 -0
  26. package/src/preferences/PreferencesEditor.ts +6 -0
  27. package/src/preferences/Types.ts +39 -0
  28. package/src/preferences/index.ts +4 -0
  29. package/types/src/epub/EpubNavigator.d.ts +27 -3
  30. package/types/src/epub/css/Properties.d.ts +177 -0
  31. package/types/src/epub/css/ReadiumCSS.d.ts +32 -0
  32. package/types/src/epub/css/index.d.ts +2 -0
  33. package/types/src/epub/frame/FrameBlobBuilder.d.ts +5 -1
  34. package/types/src/epub/frame/FrameManager.d.ts +4 -0
  35. package/types/src/epub/frame/FramePoolManager.d.ts +8 -1
  36. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +1 -3
  37. package/types/src/epub/index.d.ts +2 -0
  38. package/types/src/epub/preferences/EpubDefaults.d.ts +84 -0
  39. package/types/src/epub/preferences/EpubPreferences.d.ts +88 -0
  40. package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +54 -0
  41. package/types/src/epub/preferences/EpubSettings.d.ts +87 -0
  42. package/types/src/epub/preferences/guards.d.ts +9 -0
  43. package/types/src/epub/preferences/index.d.ts +4 -0
  44. package/types/src/helpers/dimensions.d.ts +7 -0
  45. package/types/src/helpers/index.d.ts +1 -0
  46. package/types/src/helpers/lineLength.d.ts +68 -0
  47. package/types/src/helpers/sML.d.ts +6 -1
  48. package/types/src/index.d.ts +1 -0
  49. package/types/src/preferences/Configurable.d.ts +13 -0
  50. package/types/src/preferences/Preference.d.ts +117 -0
  51. package/types/src/preferences/PreferencesEditor.d.ts +5 -0
  52. package/types/src/preferences/PreferencesSerializer.d.ts +5 -0
  53. package/types/src/preferences/Types.d.ts +24 -0
  54. package/types/src/preferences/index.d.ts +4 -0
  55. package/LICENSE +0 -28
@@ -0,0 +1,212 @@
1
+ import { ConfigurableSettings } from "../../preferences/Configurable";
2
+ import { LayoutStrategy, TextAlignment, Theme } from "../../preferences/Types";
3
+ import { EpubDefaults } from "./EpubDefaults";
4
+ import { EpubPreferences } from "./EpubPreferences";
5
+
6
+ import { sMLWithRequest } from "../../helpers";
7
+
8
+ export interface IEpubSettings {
9
+ backgroundColor?: string | null,
10
+ blendFilter?: boolean | null,
11
+ columnCount?: number | null,
12
+ constraint?: number | null,
13
+ darkenFilter?: boolean | number | null,
14
+ deprecatedFontSize?: boolean | null,
15
+ fontFamily?: string | null,
16
+ fontSize?: number | null,
17
+ fontSizeNormalize?: boolean | null,
18
+ fontOpticalSizing?: boolean | null,
19
+ fontOverride?: boolean | null,
20
+ fontWeight?: number | null,
21
+ fontWidth?: number | null,
22
+ hyphens?: boolean | null,
23
+ invertFilter?: boolean | number | null,
24
+ invertGaijiFilter: boolean | number | null,
25
+ iPadOSPatch?: boolean | null,
26
+ layoutStrategy?: LayoutStrategy | null,
27
+ letterSpacing?: number | null,
28
+ ligatures?: boolean | null,
29
+ lineHeight?: number | null,
30
+ lineLength?: number | null,
31
+ linkColor?: string | null,
32
+ maximalLineLength?: number | null,
33
+ minimalLineLength?: number | null,
34
+ noRuby?: boolean | null,
35
+ optimalLineLength?: number | null,
36
+ pageGutter?: number | null,
37
+ paragraphIndent?: number | null,
38
+ paragraphSpacing?: number | null,
39
+ scroll?: boolean | null,
40
+ selectionBackgroundColor?: string | null,
41
+ selectionTextColor?: string | null,
42
+ textAlign?: TextAlignment | null,
43
+ textColor?: string | null,
44
+ textNormalization?: boolean | null,
45
+ theme?: Theme | null,
46
+ visitedColor?: string | null,
47
+ wordSpacing?: number | null
48
+ }
49
+
50
+ export class EpubSettings implements ConfigurableSettings {
51
+ backgroundColor: string | null;
52
+ blendFilter: boolean | null;
53
+ columnCount: number | null;
54
+ constraint: number;
55
+ darkenFilter: boolean | number | null;
56
+ deprecatedFontSize: boolean | null;
57
+ fontFamily: string | null;
58
+ fontSize: number | null;
59
+ fontSizeNormalize: boolean | null;
60
+ fontOpticalSizing: boolean | null;
61
+ fontOverride: boolean | null;
62
+ fontWeight: number | null;
63
+ fontWidth: number | null;
64
+ hyphens: boolean | null;
65
+ invertFilter: boolean | number | null;
66
+ invertGaijiFilter: boolean | number | null;
67
+ iPadOSPatch: boolean;
68
+ layoutStrategy: LayoutStrategy | null;
69
+ letterSpacing: number | null;
70
+ ligatures: boolean | null;
71
+ lineHeight: number | null;
72
+ lineLength: number | null;
73
+ linkColor: string | null;
74
+ maximalLineLength: number | null;
75
+ minimalLineLength: number | null;
76
+ noRuby: boolean | null;
77
+ optimalLineLength: number;
78
+ pageGutter: number | null;
79
+ paragraphIndent: number | null;
80
+ paragraphSpacing: number | null;
81
+ scroll: boolean | null;
82
+ selectionBackgroundColor: string | null;
83
+ selectionTextColor: string | null;
84
+ textAlign: TextAlignment | null;
85
+ textColor: string | null;
86
+ textNormalization: boolean | null;
87
+ theme: Theme | null;
88
+ visitedColor: string | null;
89
+ wordSpacing: number | null;
90
+
91
+ constructor(preferences: EpubPreferences, defaults: EpubDefaults) {
92
+ this.backgroundColor = preferences.backgroundColor || defaults.backgroundColor || null;
93
+ this.blendFilter = typeof preferences.blendFilter === "boolean"
94
+ ? preferences.blendFilter
95
+ : defaults.blendFilter ?? null;
96
+ this.columnCount = preferences.columnCount !== undefined
97
+ ? preferences.columnCount
98
+ : defaults.columnCount !== undefined
99
+ ? defaults.columnCount
100
+ : null;
101
+ this.constraint = preferences.constraint || defaults.constraint;
102
+ this.darkenFilter = typeof preferences.darkenFilter === "boolean"
103
+ ? preferences.darkenFilter
104
+ : defaults.darkenFilter ?? null;
105
+ this.deprecatedFontSize = typeof preferences.deprecatedFontSize === "boolean"
106
+ ? preferences.deprecatedFontSize
107
+ : defaults.deprecatedFontSize ?? null;
108
+ this.fontFamily = preferences.fontFamily || defaults.fontFamily || null;
109
+ this.fontSize = preferences.fontSize !== undefined
110
+ ? preferences.fontSize
111
+ : defaults.fontSize !== undefined
112
+ ? defaults.fontSize
113
+ : null;
114
+ this.fontSizeNormalize = typeof preferences.fontSizeNormalize === "boolean"
115
+ ? preferences.fontSizeNormalize
116
+ : defaults.fontSizeNormalize ?? null;
117
+ this.fontOpticalSizing = typeof preferences.fontOpticalSizing === "boolean"
118
+ ? preferences.fontOpticalSizing
119
+ : defaults.fontOpticalSizing ?? null;
120
+ this.fontOverride = typeof preferences.fontOverride === "boolean"
121
+ ? preferences.fontOverride
122
+ : defaults.fontOverride ?? null;
123
+ this.fontWeight = preferences.fontWeight !== undefined
124
+ ? preferences.fontWeight
125
+ : defaults.fontWeight !== undefined
126
+ ? defaults.fontWeight
127
+ : null;
128
+ this.fontWidth = preferences.fontWidth !== undefined
129
+ ? preferences.fontWidth
130
+ : defaults.fontWidth !== undefined
131
+ ? defaults.fontWidth
132
+ : null;
133
+ this.hyphens = typeof preferences.hyphens === "boolean"
134
+ ? preferences.hyphens
135
+ : defaults.hyphens ?? null;
136
+ this.invertFilter = typeof preferences.invertFilter === "boolean"
137
+ ? preferences.invertFilter
138
+ : defaults.invertFilter ?? null;
139
+ this.invertGaijiFilter = typeof preferences.invertGaijiFilter === "boolean"
140
+ ? preferences.invertGaijiFilter
141
+ : defaults.invertGaijiFilter ?? null;
142
+ this.iPadOSPatch = this.deprecatedFontSize
143
+ ? false
144
+ : preferences.iPadOSPatch === false
145
+ ? false
146
+ : preferences.iPadOSPatch === true
147
+ ? (sMLWithRequest.OS.iPadOS && sMLWithRequest.iOSRequest === "desktop")
148
+ : defaults.iPadOSPatch;
149
+ this.layoutStrategy = preferences.layoutStrategy || defaults.layoutStrategy || null;
150
+ this.letterSpacing = preferences.letterSpacing !== undefined
151
+ ? preferences.letterSpacing
152
+ : defaults.letterSpacing !== undefined
153
+ ? defaults.letterSpacing
154
+ : null;
155
+ this.ligatures = typeof preferences.ligatures === "boolean"
156
+ ? preferences.ligatures
157
+ : defaults.ligatures ?? null;
158
+ this.lineHeight = preferences.lineHeight !== undefined
159
+ ? preferences.lineHeight
160
+ : defaults.lineHeight !== undefined
161
+ ? defaults.lineHeight
162
+ : null;
163
+ this.lineLength = preferences.lineLength !== undefined
164
+ ? preferences.lineLength
165
+ : defaults.lineLength !== undefined
166
+ ? defaults.lineLength
167
+ : null;
168
+ this.linkColor = preferences.linkColor || defaults.linkColor || null;
169
+ this.maximalLineLength = preferences.maximalLineLength === null
170
+ ? null
171
+ : preferences.maximalLineLength || defaults.maximalLineLength || null;
172
+ this.minimalLineLength = preferences.minimalLineLength === null
173
+ ? null
174
+ : preferences.minimalLineLength || defaults.minimalLineLength || null;
175
+ this.noRuby = typeof preferences.noRuby === "boolean"
176
+ ? preferences.noRuby
177
+ : defaults.noRuby ?? null;
178
+ this.optimalLineLength = preferences.optimalLineLength || defaults.optimalLineLength;
179
+ this.pageGutter = preferences.pageGutter !== undefined
180
+ ? preferences.pageGutter
181
+ : defaults.pageGutter !== undefined
182
+ ? defaults.pageGutter
183
+ : null;
184
+ this.paragraphIndent = preferences.paragraphIndent !== undefined
185
+ ? preferences.paragraphIndent
186
+ : defaults.paragraphIndent !== undefined
187
+ ? defaults.paragraphIndent
188
+ : null;
189
+ this.paragraphSpacing = preferences.paragraphSpacing !== undefined
190
+ ? preferences.paragraphSpacing
191
+ : defaults.paragraphSpacing !== undefined
192
+ ? defaults.paragraphSpacing
193
+ : null;
194
+ this.scroll = typeof preferences.scroll === "boolean"
195
+ ? preferences.scroll
196
+ : defaults.scroll ?? null;
197
+ this.selectionBackgroundColor = preferences.selectionBackgroundColor || defaults.selectionBackgroundColor || null;
198
+ this.selectionTextColor = preferences.selectionTextColor || defaults.selectionTextColor || null;
199
+ this.textAlign = preferences.textAlign || defaults.textAlign || null;
200
+ this.textColor = preferences.textColor || defaults.textColor || null;
201
+ this.textNormalization = typeof preferences.textNormalization === "boolean"
202
+ ? preferences.textNormalization
203
+ : defaults.textNormalization ?? null;
204
+ this.theme = preferences.theme || defaults.theme || null;
205
+ this.visitedColor = preferences.visitedColor || defaults.visitedColor || null;
206
+ this.wordSpacing = preferences.wordSpacing !== undefined
207
+ ? preferences.wordSpacing
208
+ : defaults.wordSpacing !== undefined
209
+ ? defaults.wordSpacing
210
+ : null;
211
+ }
212
+ }
@@ -0,0 +1,86 @@
1
+ export function ensureLessThanOrEqual<T extends number | null | undefined>(value: T, compareTo: T): T | undefined {
2
+ if (value === undefined || value === null) {
3
+ return value;
4
+ }
5
+ if (compareTo === undefined || compareTo === null) {
6
+ return value;
7
+ }
8
+ return value <= compareTo ? value : undefined;
9
+ }
10
+
11
+ export function ensureMoreThanOrEqual<T extends number | null | undefined>(value: T, compareTo: T): T | undefined {
12
+ if (value === undefined || value === null) {
13
+ return value;
14
+ }
15
+ if (compareTo === undefined || compareTo === null) {
16
+ return value;
17
+ }
18
+ return value >= compareTo ? value : undefined;
19
+ }
20
+
21
+ export function ensureString(value: string | null | undefined): string | null | undefined {
22
+ if (typeof value === "string") {
23
+ return value;
24
+ } else if (value === null) {
25
+ return null;
26
+ } else {
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ export function ensureBoolean(value: boolean | null | undefined): boolean | null | undefined {
32
+ return typeof value === "boolean"
33
+ ? value
34
+ : value === undefined || value === null
35
+ ? value
36
+ : undefined;
37
+ }
38
+
39
+
40
+ export function ensureEnumValue<T extends string>(value: T | null | undefined, enumType: Record<T, string>): T | null | undefined {
41
+ if (value === undefined) {
42
+ return undefined;
43
+ }
44
+ if (value === null) {
45
+ return null;
46
+ }
47
+ return enumType[value as T] !== undefined ? value : undefined;
48
+ }
49
+
50
+ export function ensureFilter(filter: boolean | number | null | undefined): boolean | number | null | undefined {
51
+ if (typeof filter === "boolean") {
52
+ return filter;
53
+ } else if (typeof filter === "number" && filter >= 0) {
54
+ return filter;
55
+ } else if (filter === null) {
56
+ return null;
57
+ } else {
58
+ return undefined;
59
+ }
60
+ }
61
+
62
+ export function ensureNonNegative(value: number | null | undefined): number | null | undefined {
63
+ if (value === undefined) {
64
+ return undefined;
65
+ }
66
+ if (value === null) {
67
+ return null;
68
+ }
69
+ return value < 0 ? undefined : value;
70
+ }
71
+
72
+ export function ensureValueInRange(value: number | null | undefined, range: [number, number]): number | null | undefined {
73
+ if (value === undefined) {
74
+ return undefined;
75
+ }
76
+ if (value === null) {
77
+ return null;
78
+ }
79
+ const min = Math.min(...range);
80
+ const max = Math.max(...range);
81
+ return value >= min && value <= max ? value : undefined;
82
+ }
83
+
84
+ export function withFallback<T>(value: T | null | undefined, defaultValue: T | null): T | null {
85
+ return value === undefined ? defaultValue : value;
86
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./EpubDefaults";
2
+ export * from "./EpubPreferencesEditor";
3
+ export * from "./EpubPreferences";
4
+ export * from "./EpubSettings";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Returns the "content width" of an element, which is its clientWidth
3
+ * minus any horizontal padding.
4
+ *
5
+ * @param el - The element to measure.
6
+ */
7
+ export function getContentWidth(el: Element) {
8
+ const cStyle = getComputedStyle(el);
9
+ const paddingLeft = parseFloat(cStyle.paddingLeft || "0");
10
+ const paddingRight = parseFloat(cStyle.paddingRight || "0");
11
+ return el.clientWidth - paddingLeft - paddingRight;
12
+ }
13
+
@@ -1 +1,2 @@
1
+ export * from "./lineLength";
1
2
  export * from './sML';
@@ -0,0 +1,293 @@
1
+ import fontStacks from "@readium/css/css/vars/fontStacks.json";
2
+
3
+ export interface ICustomFontFace {
4
+ name: string;
5
+ url: string;
6
+ }
7
+
8
+ export interface ILineLengthsConfig {
9
+ optimalChars: number;
10
+ minChars?: number | null;
11
+ maxChars?: number | null;
12
+ userChars?: number | null;
13
+ baseFontSize?: number | null;
14
+ sample?: string | null;
15
+ pageGutter?: number | null;
16
+ fontFace?: string | ICustomFontFace | null;
17
+ letterSpacing?: number | null;
18
+ wordSpacing?: number | null;
19
+ isCJK?: boolean | null;
20
+ getRelative?: boolean | null;
21
+ }
22
+
23
+ export interface ILineLengths {
24
+ min: number | null;
25
+ user: number | null;
26
+ max: number | null;
27
+ optimal: number;
28
+ baseFontSize: number;
29
+ }
30
+
31
+ const DEFAULT_FONT_SIZE = 16;
32
+ const DEFAULT_FONT_FACE = fontStacks.RS__oldStyleTf;
33
+
34
+ // Notes:
35
+ //
36
+ // We’re “embracing” design limitations of the ch length
37
+ // See https://developer.mozilla.org/en-US/docs/Web/CSS/length#ch
38
+ //
39
+ // Vertical-writing is not implemented yet, as it is not supported in canvas
40
+ // which means it has to be emulated by writing each character with an
41
+ // offset on the y-axis (using fillText), and getting the total height.
42
+ // If you don’t need high accuracy, it’s acceptable to use the one returned with isCJK.
43
+ //
44
+ // Instead of measuring text for min, user, and optimal each, we define multipliers
45
+ // at the end, with optimalLineLength as a ref, before returning the lineLengths object.
46
+
47
+ export class LineLengths {
48
+ private _canvas: HTMLCanvasElement;
49
+
50
+ private _optimalChars: number;
51
+ private _minChars?: number | null;
52
+ private _maxChars?: number | null;
53
+ private _userChars: number | null;
54
+ private _baseFontSize: number;
55
+ private _fontFace: string | ICustomFontFace;
56
+ private _sample: string | null;
57
+ private _pageGutter: number;
58
+ private _letterSpacing: number;
59
+ private _wordSpacing: number;
60
+ private _isCJK: boolean;
61
+ private _getRelative: boolean;
62
+
63
+ private _padding: number;
64
+ private _minDivider: number | null;
65
+ private _userMultiplier: number | null;
66
+ private _maxMultiplier: number | null;
67
+ private _approximatedWordSpaces: number;
68
+
69
+ private _optimalLineLength: number | null = null;
70
+
71
+ constructor(config: ILineLengthsConfig) {
72
+ this._canvas = document.createElement("canvas");
73
+ this._optimalChars = config.optimalChars;
74
+ this._minChars = config.minChars;
75
+ this._maxChars = config.maxChars;
76
+ this._userChars = config.userChars || null;
77
+ this._baseFontSize = config.baseFontSize || DEFAULT_FONT_SIZE;
78
+ this._fontFace = config.fontFace || DEFAULT_FONT_FACE;
79
+ this._sample = config.sample || null;
80
+ this._pageGutter = config.pageGutter || 0;
81
+ this._letterSpacing = config.letterSpacing
82
+ ? Math.round(config.letterSpacing * this._baseFontSize)
83
+ : 0;
84
+ this._wordSpacing = config.wordSpacing
85
+ ? Math.round(config.wordSpacing * this._baseFontSize)
86
+ : 0;
87
+ this._isCJK = config.isCJK || false;
88
+ this._getRelative = config.getRelative || false;
89
+ this._padding = this._pageGutter * 2;
90
+ this._minDivider = this._minChars && this._minChars < this._optimalChars
91
+ ? this._optimalChars / this._minChars
92
+ : this._minChars === null
93
+ ? null
94
+ : 1;
95
+ this._userMultiplier = this._userChars
96
+ ? this._userChars / this._optimalChars
97
+ : null;
98
+ this._maxMultiplier = this._maxChars && this._maxChars > this._optimalChars
99
+ ? this._maxChars / this._optimalChars
100
+ : this._maxChars === null
101
+ ? null
102
+ : 1;
103
+ this._approximatedWordSpaces = LineLengths.approximateWordSpaces(this._optimalChars, this._sample);
104
+ }
105
+
106
+ set minChars(n: number | null) {
107
+ if (n === this._minChars) return;
108
+ this._minChars = n;
109
+ this._minDivider = this._minChars && this._minChars < this._optimalChars
110
+ ? this._optimalChars / this._minChars
111
+ : this._minChars === null
112
+ ? null
113
+ : 1;
114
+ }
115
+
116
+ set optimalChars(n: number) {
117
+ if (n === this._optimalChars) return;
118
+ this._optimalChars = n;
119
+ this._optimalLineLength = this.getOptimalLineLength();
120
+ }
121
+
122
+ set maxChars(n: number | null) {
123
+ if (n === this._maxChars) return;
124
+ this._maxChars = n;
125
+ this._maxMultiplier = this._maxChars && this._maxChars > this._optimalChars
126
+ ? this._maxChars / this._optimalChars
127
+ : this._maxChars === null
128
+ ? null
129
+ : 1;
130
+ }
131
+
132
+ set userChars(n: number | null) {
133
+ if (n === this._userChars) return;
134
+ this._userChars = n;
135
+ this._userMultiplier = this._userChars ? this._userChars / this._optimalChars : null;
136
+ }
137
+
138
+ set letterSpacing(n: number) {
139
+ if (n === this._letterSpacing) return;
140
+ this._letterSpacing = Math.round(n * this._baseFontSize);
141
+ this._optimalLineLength = this.getOptimalLineLength();
142
+ }
143
+
144
+ set wordSpacing(n: number) {
145
+ if (n === this._wordSpacing) return;
146
+ this._wordSpacing = Math.round(n * this._baseFontSize);
147
+ this._optimalLineLength = this.getOptimalLineLength();
148
+ }
149
+
150
+ set baseFontSize(n: number) {
151
+ this._baseFontSize = n;
152
+ this._optimalLineLength = this.getOptimalLineLength();
153
+ }
154
+
155
+ set fontFace(f: string | ICustomFontFace | null) {
156
+ this._fontFace = f || DEFAULT_FONT_FACE;
157
+ this._optimalLineLength = this.getOptimalLineLength();
158
+ }
159
+
160
+ set sample(s: string) {
161
+ if (s === this._sample) return;
162
+ this._sample = s;
163
+ this._approximatedWordSpaces = LineLengths.approximateWordSpaces(this._optimalChars, this._sample);
164
+ }
165
+
166
+ set pageGutter(n: number) {
167
+ if (n === this._pageGutter) return;
168
+ this._pageGutter = n;
169
+ this._padding = this._pageGutter * 2;
170
+ this._optimalLineLength = this.getOptimalLineLength();
171
+ }
172
+
173
+ set relativeGetters(b: boolean) {
174
+ if (b === this._getRelative) return;
175
+ this._getRelative = b;
176
+ }
177
+
178
+ get baseFontSize() {
179
+ return this._baseFontSize;
180
+ }
181
+
182
+ get minimalLineLength(): number | null {
183
+ if (!this._optimalLineLength) {
184
+ this._optimalLineLength = this.getOptimalLineLength();
185
+ }
186
+ return this._minDivider !== null
187
+ ? Math.round((this._optimalLineLength / this._minDivider) + this._padding) / (this._getRelative ? this._baseFontSize : 1)
188
+ : null;
189
+ }
190
+
191
+ get userLineLength(): number | null {
192
+ if (!this._optimalLineLength) {
193
+ this._optimalLineLength = this.getOptimalLineLength();
194
+ }
195
+ return this._userMultiplier !== null
196
+ ? Math.round((this._optimalLineLength * this._userMultiplier) + this._padding) / (this._getRelative ? this._baseFontSize : 1)
197
+ : null;
198
+ }
199
+
200
+ get maximalLineLength(): number | null {
201
+ if (!this._optimalLineLength) {
202
+ this._optimalLineLength = this.getOptimalLineLength();
203
+ }
204
+ return this._maxMultiplier !== null
205
+ ? Math.round((this._optimalLineLength * this._maxMultiplier) + this._padding) / (this._getRelative ? this._baseFontSize : 1)
206
+ : null;
207
+ }
208
+
209
+ get optimalLineLength(): number {
210
+ if (!this._optimalLineLength) {
211
+ this._optimalLineLength = this.getOptimalLineLength();
212
+ }
213
+ return Math.round(this._optimalLineLength + this._padding) / (this._getRelative ? this._baseFontSize : 1);
214
+ }
215
+
216
+ get all(): ILineLengths {
217
+ if (!this._optimalLineLength) {
218
+ this._optimalLineLength = this.getOptimalLineLength();
219
+ }
220
+ return {
221
+ min: this.minimalLineLength,
222
+ user: this.userLineLength,
223
+ max: this.maximalLineLength,
224
+ optimal: this.optimalLineLength,
225
+ baseFontSize: this._baseFontSize
226
+ }
227
+ }
228
+
229
+ private static approximateWordSpaces(chars: number, sample: string | null | undefined) {
230
+ let wordSpaces = 0;
231
+ if (sample && sample.length >= chars) {
232
+ const spaceCount = sample.match(/([\s]+)/gi);
233
+ // Average for number of chars
234
+ wordSpaces = (spaceCount ? spaceCount.length : 0) * (chars / sample.length);
235
+ }
236
+ return wordSpaces;
237
+ }
238
+
239
+ private getLineLengthFallback() {
240
+ const letterSpace = this._letterSpacing * (this._optimalChars - 1);
241
+ const wordSpace = this._wordSpacing * this._approximatedWordSpaces;
242
+ return (this._optimalChars * (this._baseFontSize * 0.5)) + letterSpace + wordSpace;
243
+ }
244
+
245
+ private getOptimalLineLength() {
246
+ if (this._fontFace) {
247
+ // We know the font and can use canvas as a proxy
248
+ // to get the optimal width for the number of characters
249
+ if (typeof this._fontFace === "string") {
250
+ return this.measureText(this._fontFace);
251
+ } else {
252
+ const customFont = new FontFace(this._fontFace.name, `url(${this._fontFace.url})`);
253
+ customFont.load().then(
254
+ () => {
255
+ document.fonts.add(customFont);
256
+ return this.measureText(customFont.family)
257
+ },
258
+ (_err) => {});
259
+ }
260
+ }
261
+
262
+ return this.getLineLengthFallback();
263
+ }
264
+
265
+ private measureText(fontFace: string | null) {
266
+ // Note: We don’t clear the canvas since we’re not filling it, just measuring
267
+ const ctx: CanvasRenderingContext2D | null = this._canvas.getContext("2d");
268
+ if (ctx && fontFace) {
269
+ // ch based on 0, ic based on water ideograph
270
+ let txt = this._isCJK ? "水".repeat(this._optimalChars) : "0".repeat(this._optimalChars);
271
+ ctx.font = `${this._baseFontSize}px ${fontFace}`;
272
+
273
+ if (this._sample && this._sample.length >= this._optimalChars) {
274
+ txt = this._sample.slice(0, this._optimalChars);
275
+ }
276
+
277
+ // Not supported in Safari
278
+ if (Object.hasOwn(ctx, "letterSpacing") && Object.hasOwn(ctx, "wordSpacing")) {
279
+ ctx.letterSpacing = this._letterSpacing.toString() + "px";
280
+ ctx.wordSpacing = this._wordSpacing.toString() + "px";
281
+ return ctx.measureText(txt).width;
282
+ } else {
283
+ // Instead of filling text with an offset for each character and space
284
+ // We simply add them to the measured width since we don’t need high accuracy
285
+ const letterSpace = this._letterSpacing * (this._optimalChars - 1);
286
+ const wordSpace = this._wordSpacing * LineLengths.approximateWordSpaces(this._optimalChars, this._sample);
287
+ return ctx.measureText(txt).width + letterSpace + wordSpace;
288
+ }
289
+ } else {
290
+ return this.getLineLengthFallback();
291
+ }
292
+ }
293
+ }
@@ -44,6 +44,8 @@ declare interface UAFlags {
44
44
  LINE: number[];
45
45
  }
46
46
 
47
+ declare type iOSRequest = "mobile" | "desktop" | undefined;
48
+
47
49
  class sMLFactory {
48
50
  OS: OSFlags;
49
51
  UA: UAFlags;
@@ -117,5 +119,20 @@ class sMLFactory {
117
119
  }
118
120
  }
119
121
 
122
+ class sMLFactoryWithRequest extends sMLFactory {
123
+ get iOSRequest(): iOSRequest {
124
+ const NUAD = (navigator as any).userAgentData, NUA = navigator.userAgent;
125
+
126
+ if (this.OS.iOS && !this.OS.iPadOS) {
127
+ return "mobile";
128
+ } else if (this.OS.iPadOS) {
129
+ return (/\(iPad;/.test(NUA) || (NUAD && /^iPad(OS)?$/.test(NUAD.platform))) ? "mobile" : "desktop"
130
+ }
131
+
132
+ return undefined;
133
+ }
134
+ }
135
+
120
136
  const sML = new sMLFactory();
121
- export { sML };
137
+ const sMLWithRequest = new sMLFactoryWithRequest();
138
+ export { sML, sMLWithRequest };
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './Navigator';
2
2
  export * from './epub';
3
3
  export * from './audio';
4
- export * from './helpers';
4
+ export * from './helpers';
5
+ export * from './preferences';
@@ -0,0 +1,16 @@
1
+ import { IPreferencesEditor } from "./PreferencesEditor";
2
+
3
+ export interface ConfigurableSettings {
4
+ [key: string]: any;
5
+ }
6
+
7
+ export interface ConfigurablePreferences {
8
+ [key: string]: any;
9
+ merging(other: ConfigurablePreferences): ConfigurablePreferences;
10
+ }
11
+
12
+ export interface Configurable<ConfigurableSettings, ConfigurablePreferences> {
13
+ settings: ConfigurableSettings;
14
+ submitPreferences(preferences: ConfigurablePreferences): void;
15
+ preferencesEditor: IPreferencesEditor;
16
+ }