react-native-readium 4.0.1 → 5.0.0-rc.0
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/README.md +93 -9
- package/android/build.gradle +32 -29
- package/android/gradle.properties +3 -3
- package/android/src/main/java/com/reactnativereadium/ReadiumView.kt +88 -28
- package/android/src/main/java/com/reactnativereadium/ReadiumViewManager.kt +20 -19
- package/android/src/main/java/com/reactnativereadium/reader/BaseReaderFragment.kt +27 -6
- package/android/src/main/java/com/reactnativereadium/reader/EpubReaderFragment.kt +53 -69
- package/android/src/main/java/com/reactnativereadium/reader/PositionLabelManager.kt +132 -0
- package/android/src/main/java/com/reactnativereadium/reader/ReaderService.kt +64 -65
- package/android/src/main/java/com/reactnativereadium/reader/ReaderViewModel.kt +6 -85
- package/android/src/main/java/com/reactnativereadium/reader/VisualReaderFragment.kt +32 -2
- package/android/src/main/java/com/reactnativereadium/utils/FragmentFactory.kt +2 -3
- package/android/src/main/java/com/reactnativereadium/utils/JsonExtensions.kt +61 -0
- package/android/src/main/java/com/reactnativereadium/utils/MetadataNormalizer.kt +183 -0
- package/android/src/main/java/com/reactnativereadium/utils/NormalizedMetadata.kt +179 -0
- package/android/src/main/java/com/reactnativereadium/utils/extensions/InputStream.kt +0 -9
- package/ios/App/AppModule.swift +3 -9
- package/ios/Common/Toolkit/Extensions/Locator.swift +1 -1
- package/ios/Data/Bookmark.swift +1 -1
- package/ios/Reader/Common/ReaderViewController.swift +118 -21
- package/ios/Reader/EPUB/AssociatedColors.swift +1 -1
- package/ios/Reader/EPUB/EPUBHTTPServer.swift +13 -0
- package/ios/Reader/EPUB/EPUBModule.swift +1 -1
- package/ios/Reader/EPUB/EPUBViewController.swift +3 -4
- package/ios/Reader/ReaderFormatModule.swift +1 -1
- package/ios/Reader/ReaderModule.swift +1 -1
- package/ios/Reader/ReaderService.swift +70 -35
- package/ios/ReadiumView.swift +62 -25
- package/ios/ReadiumViewManager.m +1 -1
- package/lib/src/ReadiumViewNativeComponent.d.ts +19 -0
- package/lib/src/ReadiumViewNativeComponent.js +10 -0
- package/lib/src/components/BaseReadiumView.d.ts +1 -2
- package/lib/src/components/BaseReadiumView.js +3 -7
- package/lib/src/components/ReadiumView.js +15 -15
- package/lib/src/components/ReadiumView.web.js +100 -21
- package/lib/src/interfaces/BaseReadiumViewProps.d.ts +2 -1
- package/lib/src/interfaces/Preferences.d.ts +3 -2
- package/lib/src/interfaces/PublicationMetadata.d.ts +114 -0
- package/lib/src/interfaces/PublicationMetadata.js +1 -0
- package/lib/src/interfaces/PublicationReady.d.ts +15 -0
- package/lib/src/interfaces/PublicationReady.js +1 -0
- package/lib/src/interfaces/index.d.ts +2 -0
- package/lib/src/interfaces/index.js +2 -0
- package/lib/src/utils/index.d.ts +0 -1
- package/lib/src/utils/index.js +0 -1
- package/lib/web/hooks/index.d.ts +3 -2
- package/lib/web/hooks/index.js +3 -2
- package/lib/web/hooks/useLocationObserver.d.ts +2 -1
- package/lib/web/hooks/useLocationObserver.js +18 -11
- package/lib/web/hooks/useNavigator.d.ts +12 -0
- package/lib/web/hooks/useNavigator.js +87 -0
- package/lib/web/hooks/usePositionLabel.d.ts +9 -0
- package/lib/web/hooks/usePositionLabel.js +33 -0
- package/lib/web/hooks/usePreferencesObserver.d.ts +2 -0
- package/lib/web/hooks/usePreferencesObserver.js +54 -0
- package/lib/web/utils/manifestFetcher.d.ts +8 -0
- package/lib/web/utils/manifestFetcher.js +28 -0
- package/lib/web/utils/manifestNormalizer.d.ts +8 -0
- package/lib/web/utils/manifestNormalizer.js +70 -0
- package/lib/web/utils/metadataNormalizer.d.ts +53 -0
- package/lib/web/utils/metadataNormalizer.js +220 -0
- package/lib/web/utils/navigatorListeners.d.ts +6 -0
- package/lib/web/utils/navigatorListeners.js +50 -0
- package/lib/web/utils/publicationUtils.d.ts +15 -0
- package/lib/web/utils/publicationUtils.js +39 -0
- package/package.json +24 -14
- package/react-native-readium.podspec +7 -5
- package/src/ReadiumViewNativeComponent.ts +35 -0
- package/src/components/BaseReadiumView.tsx +3 -10
- package/src/components/ReadiumView.tsx +15 -15
- package/src/components/ReadiumView.web.tsx +120 -27
- package/src/interfaces/BaseReadiumViewProps.ts +2 -1
- package/src/interfaces/Preferences.ts +3 -2
- package/src/interfaces/PublicationMetadata.ts +141 -0
- package/src/interfaces/PublicationReady.ts +18 -0
- package/src/interfaces/index.ts +2 -0
- package/src/utils/index.ts +0 -1
- package/web/hooks/index.ts +3 -2
- package/web/hooks/useLocationObserver.ts +24 -11
- package/web/hooks/useNavigator.ts +146 -0
- package/web/hooks/usePositionLabel.ts +51 -0
- package/web/hooks/usePreferencesObserver.ts +69 -0
- package/web/utils/manifestFetcher.ts +38 -0
- package/web/utils/manifestNormalizer.ts +74 -0
- package/web/utils/metadataNormalizer.ts +238 -0
- package/web/utils/navigatorListeners.ts +60 -0
- package/web/utils/publicationUtils.ts +47 -0
- package/android/src/main/java/com/reactnativereadium/search/SearchFragment.kt +0 -100
- package/android/src/main/java/com/reactnativereadium/search/SearchPagingSource.kt +0 -44
- package/android/src/main/java/com/reactnativereadium/search/SearchResultAdapter.kt +0 -68
- package/android/src/main/java/com/reactnativereadium/utils/R2DispatcherActivity.kt +0 -45
- package/android/src/main/java/com/reactnativereadium/utils/SectionDecoration.kt +0 -98
- package/android/src/main/java/com/reactnativereadium/utils/SingleClickListener.kt +0 -32
- package/android/src/main/java/com/reactnativereadium/utils/extensions/Bitmap.kt +0 -23
- package/android/src/main/java/com/reactnativereadium/utils/extensions/Context.kt +0 -16
- package/android/src/main/java/com/reactnativereadium/utils/extensions/File.kt +0 -22
- package/android/src/main/java/com/reactnativereadium/utils/extensions/Link.kt +0 -6
- package/android/src/main/java/com/reactnativereadium/utils/extensions/Metadata.kt +0 -6
- package/android/src/main/java/com/reactnativereadium/utils/extensions/URL.kt +0 -29
- package/android/src/main/java/com/reactnativereadium/utils/extensions/Uri.kt +0 -17
- package/android/src/main/res/layout/fragment_search.xml +0 -39
- package/android/src/main/res/layout/item_recycle_search.xml +0 -14
- package/ios/Common/EPUBPreferences.swift +0 -8
- package/ios/Common/Paths.swift +0 -52
- package/ios/Common/Publication.swift +0 -15
- package/ios/Common/Toolkit/Extensions/HTTPClient.swift +0 -65
- package/ios/Common/Toolkit/Extensions/UIImage.swift +0 -12
- package/ios/Common/Toolkit/Extensions/UIViewController.swift +0 -19
- package/ios/Common/Toolkit/ScreenOrientation.swift +0 -13
- package/lib/src/utils/createFragment.d.ts +0 -1
- package/lib/src/utils/createFragment.js +0 -10
- package/lib/web/hooks/useReaderRef.d.ts +0 -3
- package/lib/web/hooks/useReaderRef.js +0 -85
- package/lib/web/hooks/useSettingsObserver.d.ts +0 -2
- package/lib/web/hooks/useSettingsObserver.js +0 -44
- package/src/utils/createFragment.ts +0 -15
- package/web/hooks/useReaderRef.ts +0 -109
- package/web/hooks/useSettingsObserver.ts +0 -61
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { EpubNavigator } from '@readium/navigator';
|
|
4
|
+
import { Locator, Publication } from '@readium/shared';
|
|
5
|
+
|
|
6
|
+
import type { ReadiumProps } from '../../src/components/ReadiumView';
|
|
7
|
+
import { normalizeMetadata } from '../utils/metadataNormalizer';
|
|
8
|
+
import { fetchManifest } from '../utils/manifestFetcher';
|
|
9
|
+
import { createNavigatorListeners } from '../utils/navigatorListeners';
|
|
10
|
+
import {
|
|
11
|
+
normalizePublicationURL,
|
|
12
|
+
createPositions,
|
|
13
|
+
extractTableOfContents,
|
|
14
|
+
} from '../utils/publicationUtils';
|
|
15
|
+
|
|
16
|
+
interface RefProps
|
|
17
|
+
extends Pick<
|
|
18
|
+
ReadiumProps,
|
|
19
|
+
'file' | 'onLocationChange' | 'onPublicationReady'
|
|
20
|
+
> {
|
|
21
|
+
container: HTMLElement | null;
|
|
22
|
+
onPositionChange?: (position: number | null) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const useNavigator = ({
|
|
26
|
+
file,
|
|
27
|
+
onLocationChange,
|
|
28
|
+
onPublicationReady,
|
|
29
|
+
container,
|
|
30
|
+
onPositionChange,
|
|
31
|
+
}: RefProps) => {
|
|
32
|
+
const [navigator, setNavigator] = useState<EpubNavigator | null>(null);
|
|
33
|
+
const [positions, setPositions] = useState<Locator[]>([]);
|
|
34
|
+
const readingOrder = useRef<Locator[]>([]);
|
|
35
|
+
|
|
36
|
+
const onLocationChangeWithTotalProgression = useCallback(
|
|
37
|
+
(newLocation: Locator) => {
|
|
38
|
+
if (
|
|
39
|
+
!onLocationChange ||
|
|
40
|
+
!readingOrder.current ||
|
|
41
|
+
!newLocation.locations
|
|
42
|
+
) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let totalProgression = newLocation.locations.totalProgression;
|
|
47
|
+
|
|
48
|
+
if (!totalProgression) {
|
|
49
|
+
const newLocationIndex = readingOrder.current.findIndex(
|
|
50
|
+
(entry) => entry.href === newLocation.href
|
|
51
|
+
);
|
|
52
|
+
if (newLocationIndex < 0 || !readingOrder.current[newLocationIndex]) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const readingOrderCount = readingOrder.current.length;
|
|
56
|
+
const chapterTotalProgression =
|
|
57
|
+
readingOrder.current[newLocationIndex].locations?.totalProgression ||
|
|
58
|
+
0;
|
|
59
|
+
|
|
60
|
+
const newLocationProgression = newLocation.locations.progression || 0;
|
|
61
|
+
const intraChapterTotalProgression =
|
|
62
|
+
newLocationProgression / readingOrderCount;
|
|
63
|
+
totalProgression =
|
|
64
|
+
chapterTotalProgression + intraChapterTotalProgression;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create a new location object with the calculated totalProgression
|
|
68
|
+
const updatedLocation = {
|
|
69
|
+
...newLocation,
|
|
70
|
+
locations: {
|
|
71
|
+
...newLocation.locations,
|
|
72
|
+
progression: newLocation.locations.progression || 0,
|
|
73
|
+
totalProgression,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// @ts-ignore - Type compatibility between Readium Locator and our Locator interface
|
|
78
|
+
onLocationChange(updatedLocation);
|
|
79
|
+
},
|
|
80
|
+
[onLocationChange]
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
async function initializeNavigator() {
|
|
85
|
+
if (!container) return;
|
|
86
|
+
|
|
87
|
+
// 1. Normalize the publication URL
|
|
88
|
+
const publicationURL = normalizePublicationURL(file.url);
|
|
89
|
+
|
|
90
|
+
// 2. Fetch and deserialize the manifest
|
|
91
|
+
const { manifest, fetcher } = await fetchManifest(publicationURL);
|
|
92
|
+
|
|
93
|
+
// 3. Create the publication
|
|
94
|
+
const publication = new Publication({ manifest, fetcher });
|
|
95
|
+
|
|
96
|
+
// 4. Create positions array for navigation
|
|
97
|
+
const positionsArray = createPositions(publication);
|
|
98
|
+
readingOrder.current = positionsArray;
|
|
99
|
+
setPositions(positionsArray);
|
|
100
|
+
|
|
101
|
+
// 5. Create navigator listeners
|
|
102
|
+
const listeners = createNavigatorListeners(
|
|
103
|
+
onLocationChangeWithTotalProgression,
|
|
104
|
+
onPositionChange
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// 6. Initialize and load the navigator
|
|
108
|
+
const configuration = {
|
|
109
|
+
preferences: { scroll: false },
|
|
110
|
+
defaults: {},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const nav = new EpubNavigator(
|
|
114
|
+
container,
|
|
115
|
+
publication,
|
|
116
|
+
listeners,
|
|
117
|
+
positionsArray,
|
|
118
|
+
undefined, // initialPosition
|
|
119
|
+
configuration as any
|
|
120
|
+
);
|
|
121
|
+
await nav.load();
|
|
122
|
+
|
|
123
|
+
// 7. Emit onPublicationReady event
|
|
124
|
+
if (onPublicationReady) {
|
|
125
|
+
const tocItems = extractTableOfContents(manifest);
|
|
126
|
+
const metadata = normalizeMetadata(manifest.metadata);
|
|
127
|
+
|
|
128
|
+
// @ts-ignore - Type compatibility between Readium types and our interfaces
|
|
129
|
+
onPublicationReady({
|
|
130
|
+
// @ts-ignore
|
|
131
|
+
tableOfContents: tocItems,
|
|
132
|
+
// @ts-ignore
|
|
133
|
+
positions: positionsArray,
|
|
134
|
+
metadata: metadata,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setNavigator(nav);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
initializeNavigator();
|
|
142
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
143
|
+
}, [file.url, container]);
|
|
144
|
+
|
|
145
|
+
return { navigator, positions };
|
|
146
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { EpubNavigator } from '@readium/navigator';
|
|
3
|
+
import type { Locator } from '../../src/interfaces';
|
|
4
|
+
|
|
5
|
+
interface PositionLabelData {
|
|
6
|
+
currentPosition: number | null;
|
|
7
|
+
totalPositions: number;
|
|
8
|
+
label: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const usePositionLabel = (
|
|
12
|
+
navigator: EpubNavigator | null,
|
|
13
|
+
positionsArray: Locator[]
|
|
14
|
+
): PositionLabelData => {
|
|
15
|
+
const [currentPosition, setCurrentPosition] = useState<number | null>(null);
|
|
16
|
+
const [totalPositions, setTotalPositions] = useState<number>(0);
|
|
17
|
+
|
|
18
|
+
// Update total positions when positions array changes
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (positionsArray && positionsArray.length > 0) {
|
|
21
|
+
setTotalPositions(positionsArray.length);
|
|
22
|
+
}
|
|
23
|
+
}, [positionsArray]);
|
|
24
|
+
|
|
25
|
+
// Listen to navigator position changes
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!navigator) return;
|
|
28
|
+
|
|
29
|
+
// Get initial position if available
|
|
30
|
+
const initialPosition = (navigator as any).currentLocation?.locations
|
|
31
|
+
?.position;
|
|
32
|
+
if (initialPosition) {
|
|
33
|
+
setCurrentPosition(initialPosition);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Note: The positionChanged listener is already set up in useNavigator
|
|
37
|
+
// We'll need to expose position updates through a callback
|
|
38
|
+
}, [navigator]);
|
|
39
|
+
|
|
40
|
+
// Generate label text
|
|
41
|
+
const label =
|
|
42
|
+
currentPosition && totalPositions > 0
|
|
43
|
+
? `${currentPosition} / ${totalPositions}`
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
currentPosition,
|
|
48
|
+
totalPositions,
|
|
49
|
+
label,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useDeepCompareEffect } from 'use-deep-compare';
|
|
2
|
+
|
|
3
|
+
import { EpubNavigator, EpubPreferences } from '@readium/navigator';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Theme color mappings
|
|
7
|
+
*/
|
|
8
|
+
const THEME_COLORS = {
|
|
9
|
+
light: {
|
|
10
|
+
backgroundColor: '#ffffff',
|
|
11
|
+
textColor: '#000000',
|
|
12
|
+
},
|
|
13
|
+
dark: {
|
|
14
|
+
backgroundColor: '#000000',
|
|
15
|
+
textColor: '#ffffff',
|
|
16
|
+
},
|
|
17
|
+
sepia: {
|
|
18
|
+
backgroundColor: '#f4ecd8',
|
|
19
|
+
textColor: '#5f4b32',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Base value for scaling pageMargins multiplier to pixels
|
|
24
|
+
// Matches Readium's default pageGutter of 20px
|
|
25
|
+
const PAGE_GUTTER_BASE = 20;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Maps our app's preferences to the navigator's expected format
|
|
29
|
+
*/
|
|
30
|
+
function mapPreferencesToNavigator(preferences: any): EpubPreferences {
|
|
31
|
+
const mapped: any = { ...preferences };
|
|
32
|
+
|
|
33
|
+
// Map pageMargins to pageGutter (the navigator uses pageGutter, not pageMargins)
|
|
34
|
+
// Our app uses a multiplier (0.5-4.0), but Readium expects pixel values
|
|
35
|
+
// Scale the multiplier to pixels: multiplier * base (e.g., 1.0 * 20 = 20px)
|
|
36
|
+
if (preferences.pageMargins !== undefined) {
|
|
37
|
+
mapped.pageGutter = preferences.pageMargins * PAGE_GUTTER_BASE;
|
|
38
|
+
delete mapped.pageMargins;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Convert theme to backgroundColor and textColor
|
|
42
|
+
// Only apply if backgroundColor/textColor aren't explicitly set
|
|
43
|
+
if (
|
|
44
|
+
preferences.theme &&
|
|
45
|
+
!preferences.backgroundColor &&
|
|
46
|
+
!preferences.textColor
|
|
47
|
+
) {
|
|
48
|
+
const themeColors =
|
|
49
|
+
THEME_COLORS[preferences.theme as keyof typeof THEME_COLORS];
|
|
50
|
+
if (themeColors) {
|
|
51
|
+
mapped.backgroundColor = themeColors.backgroundColor;
|
|
52
|
+
mapped.textColor = themeColors.textColor;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return mapped as EpubPreferences;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const usePreferencesObserver = (
|
|
60
|
+
navigator?: EpubNavigator | null,
|
|
61
|
+
preferences?: any
|
|
62
|
+
) => {
|
|
63
|
+
useDeepCompareEffect(() => {
|
|
64
|
+
if (navigator && preferences) {
|
|
65
|
+
const mappedPreferences = mapPreferencesToNavigator(preferences);
|
|
66
|
+
navigator?.submitPreferences(mappedPreferences);
|
|
67
|
+
}
|
|
68
|
+
}, [preferences, !!navigator]);
|
|
69
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Fetcher, HttpFetcher, Link, Manifest } from '@readium/shared';
|
|
2
|
+
import { normalizeManifest } from './manifestNormalizer';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fetches and deserializes the publication manifest
|
|
6
|
+
*/
|
|
7
|
+
export async function fetchManifest(publicationURL: string): Promise<{
|
|
8
|
+
manifest: Manifest;
|
|
9
|
+
fetcher: Fetcher;
|
|
10
|
+
}> {
|
|
11
|
+
const manifestLink = new Link({ href: 'manifest.json' });
|
|
12
|
+
const fetcher: Fetcher = new HttpFetcher(undefined, publicationURL);
|
|
13
|
+
const fetched = fetcher.get(manifestLink);
|
|
14
|
+
const selfLink = (await fetched.link()).toURL(publicationURL)!;
|
|
15
|
+
|
|
16
|
+
const response = await fetched.readAsJSON();
|
|
17
|
+
const responseObj = normalizeManifest(response as any);
|
|
18
|
+
|
|
19
|
+
let manifest;
|
|
20
|
+
try {
|
|
21
|
+
manifest = Manifest.deserialize(responseObj as string);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Error during manifest deserialization:', error);
|
|
24
|
+
console.error('Manifest that failed:', responseObj);
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!manifest) {
|
|
29
|
+
console.error(
|
|
30
|
+
'Failed to deserialize manifest (returned null/undefined):',
|
|
31
|
+
responseObj
|
|
32
|
+
);
|
|
33
|
+
throw new Error('Manifest deserialization returned null/undefined');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
manifest.setSelfLink(selfLink);
|
|
37
|
+
return { manifest, fetcher };
|
|
38
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a Readium manifest object to ensure compatibility with the navigator.
|
|
3
|
+
* Handles various inconsistencies in manifest formats:
|
|
4
|
+
* - Converts string properties to arrays where required
|
|
5
|
+
* - Normalizes date formats to ISO 8601
|
|
6
|
+
* - Ensures required properties exist
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeManifest(manifest: any): any {
|
|
9
|
+
const responseObj = manifest;
|
|
10
|
+
const metadata = responseObj.metadata;
|
|
11
|
+
|
|
12
|
+
// Normalize root-level properties
|
|
13
|
+
if (typeof responseObj.conformsTo === 'string') {
|
|
14
|
+
responseObj.conformsTo = [responseObj.conformsTo];
|
|
15
|
+
}
|
|
16
|
+
if (typeof metadata?.conformsTo === 'string') {
|
|
17
|
+
metadata.conformsTo = [metadata.conformsTo];
|
|
18
|
+
}
|
|
19
|
+
if (typeof metadata?.accessMode === 'string') {
|
|
20
|
+
metadata.accessMode = [metadata.accessMode];
|
|
21
|
+
}
|
|
22
|
+
if (typeof metadata?.accessibilityFeature === 'string') {
|
|
23
|
+
metadata.accessibilityFeature = [metadata.accessibilityFeature];
|
|
24
|
+
}
|
|
25
|
+
if (typeof metadata?.accessibilityHazard === 'string') {
|
|
26
|
+
metadata.accessibilityHazard = [metadata.accessibilityHazard];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Normalize nested accessibility properties
|
|
30
|
+
const accessibility = metadata?.accessibility;
|
|
31
|
+
if (accessibility) {
|
|
32
|
+
if (typeof accessibility.conformsTo === 'string') {
|
|
33
|
+
accessibility.conformsTo = [accessibility.conformsTo];
|
|
34
|
+
}
|
|
35
|
+
if (typeof accessibility.accessMode === 'string') {
|
|
36
|
+
accessibility.accessMode = [accessibility.accessMode];
|
|
37
|
+
}
|
|
38
|
+
if (typeof accessibility.feature === 'string') {
|
|
39
|
+
accessibility.feature = [accessibility.feature];
|
|
40
|
+
}
|
|
41
|
+
if (typeof accessibility.hazard === 'string') {
|
|
42
|
+
accessibility.hazard = [accessibility.hazard];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Normalize date formats
|
|
47
|
+
// Some manifests use JavaScript Date.toString() format instead of ISO 8601
|
|
48
|
+
if (metadata?.modified && typeof metadata.modified === 'string') {
|
|
49
|
+
const date = new Date(metadata.modified);
|
|
50
|
+
if (!isNaN(date.getTime())) {
|
|
51
|
+
metadata.modified = date.toISOString();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (metadata?.published && typeof metadata.published === 'string') {
|
|
55
|
+
const date = new Date(metadata.published);
|
|
56
|
+
if (!isNaN(date.getTime())) {
|
|
57
|
+
metadata.published = date.toISOString();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ensure links property exists (required by RWPM spec)
|
|
62
|
+
// Some manifests may have landmarks instead of links
|
|
63
|
+
if (!responseObj.links) {
|
|
64
|
+
// If landmarks exist, convert them to links
|
|
65
|
+
if (responseObj.landmarks && Array.isArray(responseObj.landmarks)) {
|
|
66
|
+
responseObj.links = responseObj.landmarks;
|
|
67
|
+
} else {
|
|
68
|
+
// Otherwise create an empty links array
|
|
69
|
+
responseObj.links = [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return responseObj;
|
|
74
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes metadata to ensure consistent structure per RWPM spec.
|
|
3
|
+
*
|
|
4
|
+
* This normalizer is library-agnostic and based on the stable RWPM specification,
|
|
5
|
+
* ensuring it works regardless of how the underlying Readium library serializes data.
|
|
6
|
+
*
|
|
7
|
+
* https://readium.org/webpub-manifest/
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalizes a localized string per RWPM spec.
|
|
12
|
+
*
|
|
13
|
+
* Per RWPM spec, a localized string can be:
|
|
14
|
+
* 1. Plain string: "Moby-Dick"
|
|
15
|
+
* 2. Language map: {"en": "Moby-Dick", "fr": "Moby Dick"}
|
|
16
|
+
*
|
|
17
|
+
* Priority for language selection:
|
|
18
|
+
* 1. "undefined" or "und" (default/undetermined language)
|
|
19
|
+
* 2. "en" (English fallback)
|
|
20
|
+
* 3. First available language
|
|
21
|
+
*
|
|
22
|
+
* https://readium.org/webpub-manifest/contexts/default/
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeLocalizedString(value: any): string {
|
|
25
|
+
if (!value) return '';
|
|
26
|
+
|
|
27
|
+
// Format 1: Plain string (spec-compliant shorthand)
|
|
28
|
+
if (typeof value === 'string') return value;
|
|
29
|
+
|
|
30
|
+
// Format 2: Language map (spec-compliant full form)
|
|
31
|
+
if (typeof value === 'object') {
|
|
32
|
+
// Check if it's a LocalizedString instance with translations property
|
|
33
|
+
if (value.translations) {
|
|
34
|
+
const translations = value.translations;
|
|
35
|
+
return (
|
|
36
|
+
translations['undefined'] ||
|
|
37
|
+
translations['und'] ||
|
|
38
|
+
translations['en'] ||
|
|
39
|
+
(Object.values(translations)[0] as string) ||
|
|
40
|
+
''
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if it's already a plain language map object
|
|
45
|
+
return (
|
|
46
|
+
value['undefined'] ||
|
|
47
|
+
value['und'] ||
|
|
48
|
+
value['en'] ||
|
|
49
|
+
(Object.values(value)[0] as string) ||
|
|
50
|
+
''
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Normalizes contributors per RWPM spec.
|
|
59
|
+
*
|
|
60
|
+
* Per RWPM spec, a contributor can be:
|
|
61
|
+
* 1. Plain string: "Herman Melville"
|
|
62
|
+
* 2. Object: {"name": "Herman Melville", "sortAs": "Melville, Herman"}
|
|
63
|
+
*
|
|
64
|
+
* The Web library uses a Contributors class with an "items" property.
|
|
65
|
+
*
|
|
66
|
+
* https://readium.org/webpub-manifest/schema/contributor-object.schema.json
|
|
67
|
+
*/
|
|
68
|
+
export function normalizeContributors(value: any): any[] | undefined {
|
|
69
|
+
if (!value) return undefined;
|
|
70
|
+
|
|
71
|
+
// Handle TypeScript library's Contributors class format (has .items property)
|
|
72
|
+
const items = value.items || value;
|
|
73
|
+
|
|
74
|
+
if (!Array.isArray(items)) return undefined;
|
|
75
|
+
|
|
76
|
+
return items
|
|
77
|
+
.map((contributor: any) => {
|
|
78
|
+
// Format 1: Plain string
|
|
79
|
+
if (typeof contributor === 'string') {
|
|
80
|
+
return { name: contributor };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Format 2: Object with potentially localized fields
|
|
84
|
+
const normalized: any = {
|
|
85
|
+
name: normalizeLocalizedString(contributor.name),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (contributor.sortAs) {
|
|
89
|
+
normalized.sortAs = normalizeLocalizedString(contributor.sortAs);
|
|
90
|
+
}
|
|
91
|
+
if (contributor.identifier) {
|
|
92
|
+
normalized.identifier = contributor.identifier;
|
|
93
|
+
}
|
|
94
|
+
if (contributor.role) {
|
|
95
|
+
normalized.role = contributor.role;
|
|
96
|
+
}
|
|
97
|
+
if (contributor.position !== undefined) {
|
|
98
|
+
normalized.position = contributor.position;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return normalized;
|
|
102
|
+
})
|
|
103
|
+
.filter((c) => c.name); // Remove contributors without names
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Normalizes subjects per RWPM spec.
|
|
108
|
+
*
|
|
109
|
+
* Subject names can be LocalizedStrings.
|
|
110
|
+
* The Web library uses a Subjects class with an "items" property.
|
|
111
|
+
*
|
|
112
|
+
* https://readium.org/webpub-manifest/schema/subject.schema.json
|
|
113
|
+
*/
|
|
114
|
+
export function normalizeSubjects(value: any): any[] | undefined {
|
|
115
|
+
if (!value) return undefined;
|
|
116
|
+
|
|
117
|
+
// Handle Subjects class format (has .items property)
|
|
118
|
+
const items = value.items || value;
|
|
119
|
+
|
|
120
|
+
if (!Array.isArray(items)) return undefined;
|
|
121
|
+
|
|
122
|
+
return items
|
|
123
|
+
.map((subject: any) => {
|
|
124
|
+
// Format 1: Plain string
|
|
125
|
+
if (typeof subject === 'string') {
|
|
126
|
+
return { name: subject };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Format 2: Object
|
|
130
|
+
const normalized: any = {
|
|
131
|
+
name: normalizeLocalizedString(subject.name),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (subject.sortAs) {
|
|
135
|
+
normalized.sortAs = normalizeLocalizedString(subject.sortAs);
|
|
136
|
+
}
|
|
137
|
+
if (subject.code) {
|
|
138
|
+
normalized.code = subject.code;
|
|
139
|
+
}
|
|
140
|
+
if (subject.scheme) {
|
|
141
|
+
normalized.scheme = subject.scheme;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return normalized;
|
|
145
|
+
})
|
|
146
|
+
.filter((s) => s.name);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Normalizes the entire metadata object per RWPM spec.
|
|
151
|
+
*
|
|
152
|
+
* This function is library-agnostic and based on the stable RWPM specification,
|
|
153
|
+
* ensuring it works regardless of how the underlying Readium library serializes data.
|
|
154
|
+
*
|
|
155
|
+
* https://readium.org/webpub-manifest/
|
|
156
|
+
*/
|
|
157
|
+
export function normalizeMetadata(metadata: any): any {
|
|
158
|
+
const normalized: any = {
|
|
159
|
+
// Title is required
|
|
160
|
+
title: normalizeLocalizedString(metadata.title),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Optional localized string fields
|
|
164
|
+
if (metadata.subtitle) {
|
|
165
|
+
normalized.subtitle = normalizeLocalizedString(metadata.subtitle);
|
|
166
|
+
}
|
|
167
|
+
if (metadata.sortAs) {
|
|
168
|
+
normalized.sortAs = normalizeLocalizedString(metadata.sortAs);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Direct copy fields
|
|
172
|
+
if (metadata.identifier) normalized.identifier = metadata.identifier;
|
|
173
|
+
if (metadata.conformsTo) normalized.conformsTo = metadata.conformsTo;
|
|
174
|
+
if (metadata.description) normalized.description = metadata.description;
|
|
175
|
+
if (metadata.readingProgression)
|
|
176
|
+
normalized.readingProgression = metadata.readingProgression;
|
|
177
|
+
if (metadata.layout) normalized.layout = metadata.layout;
|
|
178
|
+
if (metadata.duration) normalized.duration = metadata.duration;
|
|
179
|
+
if (metadata.numberOfPages) normalized.numberOfPages = metadata.numberOfPages;
|
|
180
|
+
|
|
181
|
+
// Date fields - convert to ISO strings
|
|
182
|
+
if (metadata.modified) {
|
|
183
|
+
normalized.modified =
|
|
184
|
+
metadata.modified instanceof Date
|
|
185
|
+
? metadata.modified.toISOString()
|
|
186
|
+
: metadata.modified;
|
|
187
|
+
}
|
|
188
|
+
if (metadata.published) {
|
|
189
|
+
normalized.published =
|
|
190
|
+
metadata.published instanceof Date
|
|
191
|
+
? metadata.published.toISOString()
|
|
192
|
+
: metadata.published;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Languages (Web uses plural 'languages', our interface uses singular 'language')
|
|
196
|
+
if (metadata.languages) {
|
|
197
|
+
normalized.language = metadata.languages;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Normalize all contributor types (Web uses plural names, our interface uses singular)
|
|
201
|
+
if (metadata.authors)
|
|
202
|
+
normalized.author = normalizeContributors(metadata.authors);
|
|
203
|
+
if (metadata.translators)
|
|
204
|
+
normalized.translator = normalizeContributors(metadata.translators);
|
|
205
|
+
if (metadata.editors)
|
|
206
|
+
normalized.editor = normalizeContributors(metadata.editors);
|
|
207
|
+
if (metadata.artists)
|
|
208
|
+
normalized.artist = normalizeContributors(metadata.artists);
|
|
209
|
+
if (metadata.illustrators)
|
|
210
|
+
normalized.illustrator = normalizeContributors(metadata.illustrators);
|
|
211
|
+
if (metadata.letterers)
|
|
212
|
+
normalized.letterer = normalizeContributors(metadata.letterers);
|
|
213
|
+
if (metadata.pencilers)
|
|
214
|
+
normalized.penciler = normalizeContributors(metadata.pencilers);
|
|
215
|
+
if (metadata.colorists)
|
|
216
|
+
normalized.colorist = normalizeContributors(metadata.colorists);
|
|
217
|
+
if (metadata.inkers)
|
|
218
|
+
normalized.inker = normalizeContributors(metadata.inkers);
|
|
219
|
+
if (metadata.narrators)
|
|
220
|
+
normalized.narrator = normalizeContributors(metadata.narrators);
|
|
221
|
+
if (metadata.contributors)
|
|
222
|
+
normalized.contributor = normalizeContributors(metadata.contributors);
|
|
223
|
+
if (metadata.publishers)
|
|
224
|
+
normalized.publisher = normalizeContributors(metadata.publishers);
|
|
225
|
+
if (metadata.imprints)
|
|
226
|
+
normalized.imprint = normalizeContributors(metadata.imprints);
|
|
227
|
+
|
|
228
|
+
// Normalize subjects
|
|
229
|
+
if (metadata.subjects) {
|
|
230
|
+
normalized.subject = normalizeSubjects(metadata.subjects);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Copy complex objects as-is
|
|
234
|
+
if (metadata.belongsTo) normalized.belongsTo = metadata.belongsTo;
|
|
235
|
+
if (metadata.accessibility) normalized.accessibility = metadata.accessibility;
|
|
236
|
+
|
|
237
|
+
return normalized;
|
|
238
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BasicTextSelection,
|
|
3
|
+
FrameClickEvent,
|
|
4
|
+
} from '@readium/navigator-html-injectables';
|
|
5
|
+
import { EpubNavigatorListeners } from '@readium/navigator';
|
|
6
|
+
import { Locator } from '@readium/shared';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates navigator listeners for handling navigation events
|
|
10
|
+
*/
|
|
11
|
+
export function createNavigatorListeners(
|
|
12
|
+
onLocationChangeWithTotalProgression: (locator: Locator) => void,
|
|
13
|
+
onPositionChange?: (position: number | null) => void
|
|
14
|
+
): EpubNavigatorListeners {
|
|
15
|
+
return {
|
|
16
|
+
frameLoaded: function (_wnd: Window): void {
|
|
17
|
+
// noop
|
|
18
|
+
},
|
|
19
|
+
positionChanged: function (_locator: Locator): void {
|
|
20
|
+
onLocationChangeWithTotalProgression(_locator);
|
|
21
|
+
if (onPositionChange) {
|
|
22
|
+
onPositionChange(_locator.locations?.position || null);
|
|
23
|
+
}
|
|
24
|
+
window.focus();
|
|
25
|
+
},
|
|
26
|
+
tap: function (_e: FrameClickEvent): boolean {
|
|
27
|
+
return false;
|
|
28
|
+
},
|
|
29
|
+
click: function (_e: FrameClickEvent): boolean {
|
|
30
|
+
return false;
|
|
31
|
+
},
|
|
32
|
+
zoom: function (_scale: number): void {
|
|
33
|
+
// noop
|
|
34
|
+
},
|
|
35
|
+
miscPointer: function (_amount: number): void {
|
|
36
|
+
// noop
|
|
37
|
+
},
|
|
38
|
+
scroll: function (_amount: number): void {
|
|
39
|
+
// noop
|
|
40
|
+
},
|
|
41
|
+
customEvent: function (_key: string, _data: unknown): void {},
|
|
42
|
+
handleLocator: function (locator: Locator): boolean {
|
|
43
|
+
const href = locator.href;
|
|
44
|
+
if (
|
|
45
|
+
href.startsWith('http://') ||
|
|
46
|
+
href.startsWith('https://') ||
|
|
47
|
+
href.startsWith('mailto:') ||
|
|
48
|
+
href.startsWith('tel:')
|
|
49
|
+
) {
|
|
50
|
+
if (confirm(`Open "${href}" ?`)) window.open(href, '_blank');
|
|
51
|
+
} else {
|
|
52
|
+
console.warn('Unhandled locator', locator);
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
},
|
|
56
|
+
textSelected: function (_selection: BasicTextSelection): void {
|
|
57
|
+
// noop
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|