@readium/navigator 1.3.4 → 2.0.0-beta.10

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 (57) hide show
  1. package/dist/index.js +3974 -2928
  2. package/dist/index.umd.cjs +16 -16
  3. package/package.json +10 -9
  4. package/src/Navigator.ts +11 -0
  5. package/src/epub/EpubNavigator.ts +250 -24
  6. package/src/epub/css/Properties.ts +396 -0
  7. package/src/epub/css/ReadiumCSS.ts +339 -0
  8. package/src/epub/css/index.ts +2 -0
  9. package/src/epub/frame/FrameBlobBuilder.ts +59 -9
  10. package/src/epub/frame/FrameManager.ts +23 -1
  11. package/src/epub/frame/FramePoolManager.ts +62 -4
  12. package/src/epub/fxl/FXLFramePoolManager.ts +23 -16
  13. package/src/epub/index.ts +3 -1
  14. package/src/epub/preferences/EpubDefaults.ts +165 -0
  15. package/src/epub/preferences/EpubPreferences.ts +192 -0
  16. package/src/epub/preferences/EpubPreferencesEditor.ts +534 -0
  17. package/src/epub/preferences/EpubSettings.ts +239 -0
  18. package/src/epub/preferences/guards.ts +86 -0
  19. package/src/epub/preferences/index.ts +4 -0
  20. package/src/helpers/dimensions.ts +13 -0
  21. package/src/helpers/index.ts +1 -0
  22. package/src/helpers/lineLength.ts +241 -0
  23. package/src/helpers/sML.ts +25 -3
  24. package/src/index.ts +2 -1
  25. package/src/preferences/Configurable.ts +16 -0
  26. package/src/preferences/Preference.ts +272 -0
  27. package/src/preferences/PreferencesEditor.ts +6 -0
  28. package/src/preferences/Types.ts +38 -0
  29. package/src/preferences/index.ts +4 -0
  30. package/types/src/Navigator.d.ts +9 -0
  31. package/types/src/epub/EpubNavigator.d.ts +34 -4
  32. package/types/src/epub/css/Properties.d.ts +183 -0
  33. package/types/src/epub/css/ReadiumCSS.d.ts +31 -0
  34. package/types/src/epub/css/index.d.ts +2 -0
  35. package/types/src/epub/frame/FrameBlobBuilder.d.ts +5 -1
  36. package/types/src/epub/frame/FrameManager.d.ts +4 -0
  37. package/types/src/epub/frame/FramePoolManager.d.ts +8 -1
  38. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +4 -4
  39. package/types/src/epub/index.d.ts +2 -0
  40. package/types/src/epub/preferences/EpubDefaults.d.ts +86 -0
  41. package/types/src/epub/preferences/EpubPreferences.d.ts +90 -0
  42. package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +55 -0
  43. package/types/src/epub/preferences/EpubSettings.d.ts +89 -0
  44. package/types/src/epub/preferences/guards.d.ts +9 -0
  45. package/types/src/epub/preferences/index.d.ts +4 -0
  46. package/types/src/helpers/dimensions.d.ts +7 -0
  47. package/types/src/helpers/index.d.ts +1 -0
  48. package/types/src/helpers/lineLength.d.ts +54 -0
  49. package/types/src/helpers/sML.d.ts +6 -1
  50. package/types/src/index.d.ts +1 -0
  51. package/types/src/preferences/Configurable.d.ts +13 -0
  52. package/types/src/preferences/Preference.d.ts +117 -0
  53. package/types/src/preferences/PreferencesEditor.d.ts +5 -0
  54. package/types/src/preferences/PreferencesSerializer.d.ts +5 -0
  55. package/types/src/preferences/Types.d.ts +23 -0
  56. package/types/src/preferences/index.d.ts +4 -0
  57. package/LICENSE +0 -28
@@ -0,0 +1,239 @@
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
+ fontWeight?: number | null,
20
+ fontWidth?: number | null,
21
+ hyphens?: boolean | null,
22
+ invertFilter?: boolean | number | null,
23
+ invertGaijiFilter: boolean | number | null,
24
+ iOSPatch?: boolean | null,
25
+ iPadOSPatch?: boolean | null,
26
+ layoutStrategy?: LayoutStrategy | null,
27
+ letterSpacing?: number | null,
28
+ ligatures?: boolean | null,
29
+ lineHeight?: number | null,
30
+ linkColor?: string | null,
31
+ maximalLineLength?: number | null,
32
+ minimalLineLength?: number | null,
33
+ noRuby?: boolean | null,
34
+ optimalLineLength?: number | null,
35
+ pageGutter?: number | null,
36
+ paragraphIndent?: number | null,
37
+ paragraphSpacing?: number | null,
38
+ scroll?: boolean | null,
39
+ scrollPaddingTop?: number | null,
40
+ scrollPaddingBottom?: number | null,
41
+ // scrollPaddingLeft?: number | null,
42
+ // scrollPaddingRight?: number | null,
43
+ selectionBackgroundColor?: string | null,
44
+ selectionTextColor?: string | null,
45
+ textAlign?: TextAlignment | null,
46
+ textColor?: string | null,
47
+ textNormalization?: boolean | null,
48
+ theme?: Theme | null,
49
+ visitedColor?: string | null,
50
+ wordSpacing?: number | null
51
+ }
52
+
53
+ export class EpubSettings implements ConfigurableSettings {
54
+ backgroundColor: string | null;
55
+ blendFilter: boolean | null;
56
+ columnCount: number | null;
57
+ constraint: number;
58
+ darkenFilter: boolean | number | null;
59
+ deprecatedFontSize: boolean | null;
60
+ fontFamily: string | null;
61
+ fontSize: number | null;
62
+ fontSizeNormalize: boolean | null;
63
+ fontOpticalSizing: boolean | null;
64
+ fontWeight: number | null;
65
+ fontWidth: number | null;
66
+ hyphens: boolean | null;
67
+ invertFilter: boolean | number | null;
68
+ invertGaijiFilter: boolean | number | null;
69
+ iOSPatch: boolean;
70
+ iPadOSPatch: boolean;
71
+ layoutStrategy: LayoutStrategy | null;
72
+ letterSpacing: number | null;
73
+ ligatures: boolean | null;
74
+ lineHeight: number | null;
75
+ linkColor: string | null;
76
+ maximalLineLength: number | null;
77
+ minimalLineLength: number | null;
78
+ noRuby: boolean | null;
79
+ optimalLineLength: number;
80
+ pageGutter: number | null;
81
+ paragraphIndent: number | null;
82
+ paragraphSpacing: number | null;
83
+ scroll: boolean | null;
84
+ scrollPaddingTop: number | null;
85
+ scrollPaddingBottom: number | null;
86
+ // scrollPaddingLeft: number | null;
87
+ // scrollPaddingRight: number | null;
88
+ selectionBackgroundColor: string | null;
89
+ selectionTextColor: string | null;
90
+ textAlign: TextAlignment | null;
91
+ textColor: string | null;
92
+ textNormalization: boolean | null;
93
+ theme: Theme | null;
94
+ visitedColor: string | null;
95
+ wordSpacing: number | null;
96
+
97
+ constructor(preferences: EpubPreferences, defaults: EpubDefaults) {
98
+ this.backgroundColor = preferences.backgroundColor || defaults.backgroundColor || null;
99
+ this.blendFilter = typeof preferences.blendFilter === "boolean"
100
+ ? preferences.blendFilter
101
+ : defaults.blendFilter ?? null;
102
+ this.columnCount = preferences.columnCount !== undefined
103
+ ? preferences.columnCount
104
+ : defaults.columnCount !== undefined
105
+ ? defaults.columnCount
106
+ : null;
107
+ this.constraint = preferences.constraint || defaults.constraint;
108
+ this.darkenFilter = typeof preferences.darkenFilter === "boolean"
109
+ ? preferences.darkenFilter
110
+ : defaults.darkenFilter ?? null;
111
+ this.deprecatedFontSize = typeof preferences.deprecatedFontSize === "boolean"
112
+ ? preferences.deprecatedFontSize
113
+ : defaults.deprecatedFontSize ?? null;
114
+ this.fontFamily = preferences.fontFamily || defaults.fontFamily || null;
115
+ this.fontSize = preferences.fontSize !== undefined
116
+ ? preferences.fontSize
117
+ : defaults.fontSize !== undefined
118
+ ? defaults.fontSize
119
+ : null;
120
+ this.fontSizeNormalize = typeof preferences.fontSizeNormalize === "boolean"
121
+ ? preferences.fontSizeNormalize
122
+ : defaults.fontSizeNormalize ?? null;
123
+ this.fontOpticalSizing = typeof preferences.fontOpticalSizing === "boolean"
124
+ ? preferences.fontOpticalSizing
125
+ : defaults.fontOpticalSizing ?? null;
126
+ this.fontWeight = preferences.fontWeight !== undefined
127
+ ? preferences.fontWeight
128
+ : defaults.fontWeight !== undefined
129
+ ? defaults.fontWeight
130
+ : null;
131
+ this.fontWidth = preferences.fontWidth !== undefined
132
+ ? preferences.fontWidth
133
+ : defaults.fontWidth !== undefined
134
+ ? defaults.fontWidth
135
+ : null;
136
+ this.hyphens = typeof preferences.hyphens === "boolean"
137
+ ? preferences.hyphens
138
+ : defaults.hyphens ?? null;
139
+ this.invertFilter = typeof preferences.invertFilter === "boolean"
140
+ ? preferences.invertFilter
141
+ : defaults.invertFilter ?? null;
142
+ this.invertGaijiFilter = typeof preferences.invertGaijiFilter === "boolean"
143
+ ? preferences.invertGaijiFilter
144
+ : defaults.invertGaijiFilter ?? null;
145
+ this.iOSPatch = this.deprecatedFontSize
146
+ ? false
147
+ : preferences.iOSPatch === false
148
+ ? false
149
+ : preferences.iOSPatch === true
150
+ ? ((sMLWithRequest.OS.iOS || sMLWithRequest.OS.iPadOS) && sMLWithRequest.iOSRequest === "mobile")
151
+ : defaults.iOSPatch;
152
+ this.iPadOSPatch = this.deprecatedFontSize
153
+ ? false
154
+ : preferences.iPadOSPatch === false
155
+ ? false
156
+ : preferences.iPadOSPatch === true
157
+ ? (sMLWithRequest.OS.iPadOS && sMLWithRequest.iOSRequest === "desktop")
158
+ : defaults.iPadOSPatch;
159
+ this.layoutStrategy = preferences.layoutStrategy || defaults.layoutStrategy || null;
160
+ this.letterSpacing = preferences.letterSpacing !== undefined
161
+ ? preferences.letterSpacing
162
+ : defaults.letterSpacing !== undefined
163
+ ? defaults.letterSpacing
164
+ : null;
165
+ this.ligatures = typeof preferences.ligatures === "boolean"
166
+ ? preferences.ligatures
167
+ : defaults.ligatures ?? null;
168
+ this.lineHeight = preferences.lineHeight !== undefined
169
+ ? preferences.lineHeight
170
+ : defaults.lineHeight !== undefined
171
+ ? defaults.lineHeight
172
+ : null;
173
+ this.linkColor = preferences.linkColor || defaults.linkColor || null;
174
+ this.maximalLineLength = preferences.maximalLineLength === null
175
+ ? null
176
+ : preferences.maximalLineLength || defaults.maximalLineLength || null;
177
+ this.minimalLineLength = preferences.minimalLineLength === null
178
+ ? null
179
+ : preferences.minimalLineLength || defaults.minimalLineLength || null;
180
+ this.noRuby = typeof preferences.noRuby === "boolean"
181
+ ? preferences.noRuby
182
+ : defaults.noRuby ?? null;
183
+ this.optimalLineLength = preferences.optimalLineLength || defaults.optimalLineLength;
184
+ this.pageGutter = preferences.pageGutter !== undefined
185
+ ? preferences.pageGutter
186
+ : defaults.pageGutter !== undefined
187
+ ? defaults.pageGutter
188
+ : null;
189
+ this.paragraphIndent = preferences.paragraphIndent !== undefined
190
+ ? preferences.paragraphIndent
191
+ : defaults.paragraphIndent !== undefined
192
+ ? defaults.paragraphIndent
193
+ : null;
194
+ this.paragraphSpacing = preferences.paragraphSpacing !== undefined
195
+ ? preferences.paragraphSpacing
196
+ : defaults.paragraphSpacing !== undefined
197
+ ? defaults.paragraphSpacing
198
+ : null;
199
+ this.scroll = typeof preferences.scroll === "boolean"
200
+ ? preferences.scroll
201
+ : defaults.scroll ?? null;
202
+ this.scrollPaddingTop = preferences.scrollPaddingTop !== undefined
203
+ ? preferences.scrollPaddingTop
204
+ : defaults.scrollPaddingTop !== undefined
205
+ ? defaults.scrollPaddingTop
206
+ : null;
207
+ this.scrollPaddingBottom = preferences.scrollPaddingBottom !== undefined
208
+ ? preferences.scrollPaddingBottom
209
+ : defaults.scrollPaddingBottom !== undefined
210
+ ? defaults.scrollPaddingBottom
211
+ : null;
212
+ /*
213
+ this.scrollPaddingLeft = preferences.scrollPaddingLeft !== undefined
214
+ ? preferences.scrollPaddingLeft
215
+ : defaults.scrollPaddingLeft !== undefined
216
+ ? defaults.scrollPaddingLeft
217
+ : null;
218
+ this.scrollPaddingRight = preferences.scrollPaddingRight !== undefined
219
+ ? preferences.scrollPaddingRight
220
+ : defaults.scrollPaddingRight !== undefined
221
+ ? defaults.scrollPaddingRight
222
+ : null;
223
+ */
224
+ this.selectionBackgroundColor = preferences.selectionBackgroundColor || defaults.selectionBackgroundColor || null;
225
+ this.selectionTextColor = preferences.selectionTextColor || defaults.selectionTextColor || null;
226
+ this.textAlign = preferences.textAlign || defaults.textAlign || null;
227
+ this.textColor = preferences.textColor || defaults.textColor || null;
228
+ this.textNormalization = typeof preferences.textNormalization === "boolean"
229
+ ? preferences.textNormalization
230
+ : defaults.textNormalization ?? null;
231
+ this.theme = preferences.theme || defaults.theme || null;
232
+ this.visitedColor = preferences.visitedColor || defaults.visitedColor || null;
233
+ this.wordSpacing = preferences.wordSpacing !== undefined
234
+ ? preferences.wordSpacing
235
+ : defaults.wordSpacing !== undefined
236
+ ? defaults.wordSpacing
237
+ : null;
238
+ }
239
+ }
@@ -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,241 @@
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
+ baseFontSize?: number | null;
13
+ sample?: string | null;
14
+ pageGutter?: number | null;
15
+ fontFace?: string | ICustomFontFace | null;
16
+ letterSpacing?: number | null;
17
+ wordSpacing?: number | null;
18
+ isCJK?: boolean | null;
19
+ getRelative?: boolean | null;
20
+ }
21
+
22
+ export interface ILineLengths {
23
+ min: number | null;
24
+ max: number | null;
25
+ optimal: number;
26
+ baseFontSize: number;
27
+ }
28
+
29
+ const DEFAULT_FONT_SIZE = 16;
30
+ const DEFAULT_FONT_FACE = fontStacks.RS__oldStyleTf;
31
+
32
+ // Notes:
33
+ //
34
+ // We’re “embracing” design limitations of the ch length
35
+ // See https://developer.mozilla.org/en-US/docs/Web/CSS/length#ch
36
+ //
37
+ // Vertical-writing is not implemented yet, as it is not supported in canvas
38
+ // which means it has to be emulated by writing each character with an
39
+ // offset on the y-axis (using fillText), and getting the total height.
40
+ // If you don’t need high accuracy, it’s acceptable to use the one returned with isCJK.
41
+ //
42
+ // Instead of measuring text for min and maximal, we define multipliers
43
+ // at the end, with optimalLineLength as a ref, before returning the lineLengths object.
44
+
45
+ export class LineLengths {
46
+ private _canvas: HTMLCanvasElement;
47
+
48
+ private _optimalChars: number;
49
+ private _minChars?: number | null;
50
+ private _maxChars?: number | null;
51
+ private _baseFontSize: number;
52
+ private _fontFace: string | ICustomFontFace;
53
+ private _sample: string | null;
54
+ private _pageGutter: number;
55
+ private _letterSpacing: number;
56
+ private _wordSpacing: number;
57
+ private _isCJK: boolean;
58
+ private _getRelative: boolean;
59
+
60
+ private _padding: number;
61
+ private _minDivider: number | null;
62
+ private _maxMultiplier: number | null;
63
+ private _approximatedWordSpaces: number;
64
+
65
+ private _optimalLineLength: number | null = null;
66
+
67
+ constructor(config: ILineLengthsConfig) {
68
+ this._canvas = document.createElement("canvas");
69
+ this._optimalChars = config.optimalChars;
70
+ this._minChars = config.minChars;
71
+ this._maxChars = config.maxChars;
72
+ this._baseFontSize = config.baseFontSize || DEFAULT_FONT_SIZE;
73
+ this._fontFace = config.fontFace || DEFAULT_FONT_FACE;
74
+ this._sample = config.sample || null;
75
+ this._pageGutter = config.pageGutter || 0;
76
+ this._letterSpacing = config.letterSpacing
77
+ ? Math.round(config.letterSpacing * this._baseFontSize)
78
+ : 0;
79
+ this._wordSpacing = config.wordSpacing
80
+ ? Math.round(config.wordSpacing * this._baseFontSize)
81
+ : 0;
82
+ this._isCJK = config.isCJK || false;
83
+ this._getRelative = config.getRelative || false;
84
+ this._padding = this._pageGutter * 2;
85
+ this._minDivider = this._minChars && this._minChars < this._optimalChars
86
+ ? this._optimalChars / this._minChars
87
+ : this._minChars === null
88
+ ? null
89
+ : 1;
90
+ this._maxMultiplier = this._maxChars && this._maxChars > this._optimalChars
91
+ ? this._maxChars / this._optimalChars
92
+ : this._maxChars === null
93
+ ? null
94
+ : 1;
95
+ this._approximatedWordSpaces = LineLengths.approximateWordSpaces(this._optimalChars, this._sample);
96
+ }
97
+
98
+ private updateMultipliers() {
99
+ this._minDivider = this._minChars && this._minChars < this._optimalChars
100
+ ? this._optimalChars / this._minChars
101
+ : this._minChars === null
102
+ ? null
103
+ : 1;
104
+
105
+ this._maxMultiplier = this._maxChars && this._maxChars > this._optimalChars
106
+ ? this._maxChars / this._optimalChars
107
+ : this._maxChars === null
108
+ ? null
109
+ : 1;
110
+ }
111
+
112
+ // Batch update to guarantee up-to-date values
113
+ // Not filtering because pretty much everything can
114
+ // trigger a recomputation anyway.
115
+ update(props: Partial<ILineLengthsConfig>) {
116
+ if (props.optimalChars) this._optimalChars = props.optimalChars;
117
+ if (props.minChars !== undefined) this._minChars = props.minChars;
118
+ if (props.maxChars !== undefined) this._maxChars = props.maxChars;
119
+ if (props.baseFontSize) this._baseFontSize = props.baseFontSize;
120
+ if (props.fontFace !== undefined) this._fontFace = props.fontFace || DEFAULT_FONT_FACE;
121
+ if (props.letterSpacing) this._letterSpacing = props.letterSpacing;
122
+ if (props.wordSpacing) this._wordSpacing = props.wordSpacing;
123
+ if (props.isCJK != null) this._isCJK = props.isCJK;
124
+ if (props.pageGutter) this._pageGutter = props.pageGutter;
125
+ if (props.getRelative) this._getRelative = props.getRelative;
126
+
127
+ if (props.sample) {
128
+ this._sample = props.sample;
129
+ this._approximatedWordSpaces = LineLengths.approximateWordSpaces(this._optimalChars, this._sample);
130
+ }
131
+
132
+ this.updateMultipliers();
133
+ this._optimalLineLength = this.getOptimalLineLength();
134
+ }
135
+
136
+ get baseFontSize() {
137
+ return this._baseFontSize;
138
+ }
139
+
140
+ get minimalLineLength(): number | null {
141
+ if (!this._optimalLineLength) {
142
+ this._optimalLineLength = this.getOptimalLineLength();
143
+ }
144
+ return this._minDivider !== null
145
+ ? Math.round((this._optimalLineLength / this._minDivider) + this._padding) / (this._getRelative ? this._baseFontSize : 1)
146
+ : null;
147
+ }
148
+
149
+ get maximalLineLength(): number | null {
150
+ if (!this._optimalLineLength) {
151
+ this._optimalLineLength = this.getOptimalLineLength();
152
+ }
153
+ return this._maxMultiplier !== null
154
+ ? Math.round((this._optimalLineLength * this._maxMultiplier) + this._padding) / (this._getRelative ? this._baseFontSize : 1)
155
+ : null;
156
+ }
157
+
158
+ get optimalLineLength(): number {
159
+ if (!this._optimalLineLength) {
160
+ this._optimalLineLength = this.getOptimalLineLength();
161
+ }
162
+ return Math.round(this._optimalLineLength + this._padding) / (this._getRelative ? this._baseFontSize : 1);
163
+ }
164
+
165
+ get all(): ILineLengths {
166
+ if (!this._optimalLineLength) {
167
+ this._optimalLineLength = this.getOptimalLineLength();
168
+ }
169
+ return {
170
+ min: this.minimalLineLength,
171
+ max: this.maximalLineLength,
172
+ optimal: this.optimalLineLength,
173
+ baseFontSize: this._baseFontSize
174
+ }
175
+ }
176
+
177
+ private static approximateWordSpaces(chars: number, sample: string | null | undefined) {
178
+ let wordSpaces = 0;
179
+ if (sample && sample.length >= chars) {
180
+ const spaceCount = sample.match(/([\s]+)/gi);
181
+ // Average for number of chars
182
+ wordSpaces = (spaceCount ? spaceCount.length : 0) * (chars / sample.length);
183
+ }
184
+ return wordSpaces;
185
+ }
186
+
187
+ private getLineLengthFallback() {
188
+ const letterSpace = this._letterSpacing * (this._optimalChars - 1);
189
+ const wordSpace = this._wordSpacing * this._approximatedWordSpaces;
190
+ return (this._optimalChars * (this._baseFontSize * 0.5)) + letterSpace + wordSpace;
191
+ }
192
+
193
+ private getOptimalLineLength() {
194
+ if (this._fontFace) {
195
+ // We know the font and can use canvas as a proxy
196
+ // to get the optimal width for the number of characters
197
+ if (typeof this._fontFace === "string") {
198
+ return this.measureText(this._fontFace);
199
+ } else {
200
+ const customFont = new FontFace(this._fontFace.name, `url(${this._fontFace.url})`);
201
+ customFont.load().then(
202
+ () => {
203
+ document.fonts.add(customFont);
204
+ return this.measureText(customFont.family)
205
+ },
206
+ (_err) => {});
207
+ }
208
+ }
209
+
210
+ return this.getLineLengthFallback();
211
+ }
212
+
213
+ private measureText(fontFace: string | null) {
214
+ // Note: We don’t clear the canvas since we’re not filling it, just measuring
215
+ const ctx: CanvasRenderingContext2D | null = this._canvas.getContext("2d");
216
+ if (ctx && fontFace) {
217
+ // ch based on 0, ic based on water ideograph
218
+ let txt = this._isCJK ? "水".repeat(this._optimalChars) : "0".repeat(this._optimalChars);
219
+ ctx.font = `${this._baseFontSize}px ${fontFace}`;
220
+
221
+ if (this._sample && this._sample.length >= this._optimalChars) {
222
+ txt = this._sample.slice(0, this._optimalChars);
223
+ }
224
+
225
+ // Not supported in Safari
226
+ if (Object.hasOwn(ctx, "letterSpacing") && Object.hasOwn(ctx, "wordSpacing")) {
227
+ ctx.letterSpacing = this._letterSpacing.toString() + "px";
228
+ ctx.wordSpacing = this._wordSpacing.toString() + "px";
229
+ return ctx.measureText(txt).width;
230
+ } else {
231
+ // Instead of filling text with an offset for each character and space
232
+ // We simply add them to the measured width since we don’t need high accuracy
233
+ const letterSpace = this._letterSpacing * (this._optimalChars - 1);
234
+ const wordSpace = this._wordSpacing * LineLengths.approximateWordSpaces(this._optimalChars, this._sample);
235
+ return ctx.measureText(txt).width + letterSpace + wordSpace;
236
+ }
237
+ } else {
238
+ return this.getLineLengthFallback();
239
+ }
240
+ }
241
+ }
@@ -8,6 +8,7 @@
8
8
  * Portions of this code come from the sML library
9
9
  * Current version: 1.0.36
10
10
  */
11
+ /// <reference types="user-agent-data-types" />
11
12
 
12
13
  declare interface OSFlags {
13
14
  iOS: number[];
@@ -44,13 +45,19 @@ declare interface UAFlags {
44
45
  LINE: number[];
45
46
  }
46
47
 
48
+ declare type iOSRequest = "mobile" | "desktop" | undefined;
49
+
50
+ // Fallback when global 'navigator' is not available, such as in SSR environments.
51
+ const userAgent = () => typeof navigator === "undefined" ? "" : (navigator.userAgent || "");
52
+ const userAgentData = () => typeof navigator === "undefined" ? undefined : (navigator.userAgentData || undefined);
53
+
47
54
  class sMLFactory {
48
55
  OS: OSFlags;
49
56
  UA: UAFlags;
50
57
  Env!: string[];
51
58
 
52
59
  constructor() {
53
- const NUAD = (navigator as any).userAgentData, NUA = navigator.userAgent;
60
+ const NUAD = userAgentData(), NUA = userAgent();
54
61
 
55
62
  const _sV = (V?: string | number) => (typeof V === "string" || typeof V === "number") && V ? String(V).replace(/_/g, ".").split(".").map(I => parseInt(I) || 0) : [];
56
63
  const _dV = (Pre="") => {
@@ -82,7 +89,7 @@ class sMLFactory {
82
89
  })({} as OSFlags));
83
90
 
84
91
  this.UA = ((UA: UAFlags) => { let _OK = false;
85
- if(NUAD && Array.isArray(NUAD.brands)) { const BnV = NUAD.brands.reduce((BnV: string[], _: any) => { (BnV[_.brand] as any) = [_.version * 1]; return BnV; }, {});
92
+ if(NUAD && Array.isArray(NUAD.brands)) { const BnV = NUAD.brands.reduce((BnV: Record<string, number[]>, _: NavigatorUABrandVersion) => { BnV[_.brand] = [(_.version as any) * 1]; return BnV; }, {});
86
93
  if(BnV["Google Chrome"]) _OK = true, UA.Blink = UA.Chromium = BnV["Chromium"] || [], UA.Chrome = BnV["Google Chrome"];
87
94
  else if(BnV["Microsoft Edge"]) _OK = true, UA.Blink = UA.Chromium = BnV["Chromium"] || [], UA.Edge = BnV["Microsoft Edge"];
88
95
  else if(BnV["Opera"]) _OK = true, UA.Blink = UA.Chromium = BnV["Chromium"] || [], UA.Opera = BnV["Opera"];
@@ -117,5 +124,20 @@ class sMLFactory {
117
124
  }
118
125
  }
119
126
 
127
+ class sMLFactoryWithRequest extends sMLFactory {
128
+ get iOSRequest(): iOSRequest {
129
+ const NUAD = userAgentData(), NUA = userAgent();
130
+
131
+ if (this.OS.iOS && !this.OS.iPadOS) {
132
+ return "mobile";
133
+ } else if (this.OS.iPadOS) {
134
+ return (/\(iPad;/.test(NUA) || (NUAD && /^iPad(OS)?$/.test(NUAD.platform))) ? "mobile" : "desktop"
135
+ }
136
+
137
+ return undefined;
138
+ }
139
+ }
140
+
120
141
  const sML = new sMLFactory();
121
- export { sML };
142
+ const sMLWithRequest = new sMLFactoryWithRequest();
143
+ 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
+ }