@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.
- package/dist/index.js +3533 -2595
- package/dist/index.umd.cjs +16 -16
- package/package.json +9 -9
- package/src/epub/EpubNavigator.ts +184 -7
- package/src/epub/css/Properties.ts +376 -0
- package/src/epub/css/ReadiumCSS.ts +348 -0
- package/src/epub/css/index.ts +2 -0
- package/src/epub/frame/FrameBlobBuilder.ts +59 -9
- package/src/epub/frame/FrameManager.ts +16 -0
- package/src/epub/frame/FramePoolManager.ts +61 -2
- package/src/epub/fxl/FXLFramePoolManager.ts +3 -15
- package/src/epub/index.ts +3 -1
- package/src/epub/preferences/EpubDefaults.ts +154 -0
- package/src/epub/preferences/EpubPreferences.ts +183 -0
- package/src/epub/preferences/EpubPreferencesEditor.ts +501 -0
- package/src/epub/preferences/EpubSettings.ts +212 -0
- package/src/epub/preferences/guards.ts +86 -0
- package/src/epub/preferences/index.ts +4 -0
- package/src/helpers/dimensions.ts +13 -0
- package/src/helpers/index.ts +1 -0
- package/src/helpers/lineLength.ts +293 -0
- package/src/helpers/sML.ts +18 -1
- package/src/index.ts +2 -1
- package/src/preferences/Configurable.ts +16 -0
- package/src/preferences/Preference.ts +272 -0
- package/src/preferences/PreferencesEditor.ts +6 -0
- package/src/preferences/Types.ts +39 -0
- package/src/preferences/index.ts +4 -0
- package/types/src/epub/EpubNavigator.d.ts +27 -3
- package/types/src/epub/css/Properties.d.ts +177 -0
- package/types/src/epub/css/ReadiumCSS.d.ts +32 -0
- package/types/src/epub/css/index.d.ts +2 -0
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +5 -1
- package/types/src/epub/frame/FrameManager.d.ts +4 -0
- package/types/src/epub/frame/FramePoolManager.d.ts +8 -1
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +1 -3
- package/types/src/epub/index.d.ts +2 -0
- package/types/src/epub/preferences/EpubDefaults.d.ts +84 -0
- package/types/src/epub/preferences/EpubPreferences.d.ts +88 -0
- package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +54 -0
- package/types/src/epub/preferences/EpubSettings.d.ts +87 -0
- package/types/src/epub/preferences/guards.d.ts +9 -0
- package/types/src/epub/preferences/index.d.ts +4 -0
- package/types/src/helpers/dimensions.d.ts +7 -0
- package/types/src/helpers/index.d.ts +1 -0
- package/types/src/helpers/lineLength.d.ts +68 -0
- package/types/src/helpers/sML.d.ts +6 -1
- package/types/src/index.d.ts +1 -0
- package/types/src/preferences/Configurable.d.ts +13 -0
- package/types/src/preferences/Preference.d.ts +117 -0
- package/types/src/preferences/PreferencesEditor.d.ts +5 -0
- package/types/src/preferences/PreferencesSerializer.d.ts +5 -0
- package/types/src/preferences/Types.d.ts +24 -0
- package/types/src/preferences/index.d.ts +4 -0
- 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,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
|
+
|
package/src/helpers/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/helpers/sML.ts
CHANGED
|
@@ -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
|
-
|
|
137
|
+
const sMLWithRequest = new sMLFactoryWithRequest();
|
|
138
|
+
export { sML, sMLWithRequest };
|
package/src/index.ts
CHANGED
|
@@ -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
|
+
}
|