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.
Files changed (118) hide show
  1. package/README.md +93 -9
  2. package/android/build.gradle +32 -29
  3. package/android/gradle.properties +3 -3
  4. package/android/src/main/java/com/reactnativereadium/ReadiumView.kt +88 -28
  5. package/android/src/main/java/com/reactnativereadium/ReadiumViewManager.kt +20 -19
  6. package/android/src/main/java/com/reactnativereadium/reader/BaseReaderFragment.kt +27 -6
  7. package/android/src/main/java/com/reactnativereadium/reader/EpubReaderFragment.kt +53 -69
  8. package/android/src/main/java/com/reactnativereadium/reader/PositionLabelManager.kt +132 -0
  9. package/android/src/main/java/com/reactnativereadium/reader/ReaderService.kt +64 -65
  10. package/android/src/main/java/com/reactnativereadium/reader/ReaderViewModel.kt +6 -85
  11. package/android/src/main/java/com/reactnativereadium/reader/VisualReaderFragment.kt +32 -2
  12. package/android/src/main/java/com/reactnativereadium/utils/FragmentFactory.kt +2 -3
  13. package/android/src/main/java/com/reactnativereadium/utils/JsonExtensions.kt +61 -0
  14. package/android/src/main/java/com/reactnativereadium/utils/MetadataNormalizer.kt +183 -0
  15. package/android/src/main/java/com/reactnativereadium/utils/NormalizedMetadata.kt +179 -0
  16. package/android/src/main/java/com/reactnativereadium/utils/extensions/InputStream.kt +0 -9
  17. package/ios/App/AppModule.swift +3 -9
  18. package/ios/Common/Toolkit/Extensions/Locator.swift +1 -1
  19. package/ios/Data/Bookmark.swift +1 -1
  20. package/ios/Reader/Common/ReaderViewController.swift +118 -21
  21. package/ios/Reader/EPUB/AssociatedColors.swift +1 -1
  22. package/ios/Reader/EPUB/EPUBHTTPServer.swift +13 -0
  23. package/ios/Reader/EPUB/EPUBModule.swift +1 -1
  24. package/ios/Reader/EPUB/EPUBViewController.swift +3 -4
  25. package/ios/Reader/ReaderFormatModule.swift +1 -1
  26. package/ios/Reader/ReaderModule.swift +1 -1
  27. package/ios/Reader/ReaderService.swift +70 -35
  28. package/ios/ReadiumView.swift +62 -25
  29. package/ios/ReadiumViewManager.m +1 -1
  30. package/lib/src/ReadiumViewNativeComponent.d.ts +19 -0
  31. package/lib/src/ReadiumViewNativeComponent.js +10 -0
  32. package/lib/src/components/BaseReadiumView.d.ts +1 -2
  33. package/lib/src/components/BaseReadiumView.js +3 -7
  34. package/lib/src/components/ReadiumView.js +15 -15
  35. package/lib/src/components/ReadiumView.web.js +100 -21
  36. package/lib/src/interfaces/BaseReadiumViewProps.d.ts +2 -1
  37. package/lib/src/interfaces/Preferences.d.ts +3 -2
  38. package/lib/src/interfaces/PublicationMetadata.d.ts +114 -0
  39. package/lib/src/interfaces/PublicationMetadata.js +1 -0
  40. package/lib/src/interfaces/PublicationReady.d.ts +15 -0
  41. package/lib/src/interfaces/PublicationReady.js +1 -0
  42. package/lib/src/interfaces/index.d.ts +2 -0
  43. package/lib/src/interfaces/index.js +2 -0
  44. package/lib/src/utils/index.d.ts +0 -1
  45. package/lib/src/utils/index.js +0 -1
  46. package/lib/web/hooks/index.d.ts +3 -2
  47. package/lib/web/hooks/index.js +3 -2
  48. package/lib/web/hooks/useLocationObserver.d.ts +2 -1
  49. package/lib/web/hooks/useLocationObserver.js +18 -11
  50. package/lib/web/hooks/useNavigator.d.ts +12 -0
  51. package/lib/web/hooks/useNavigator.js +87 -0
  52. package/lib/web/hooks/usePositionLabel.d.ts +9 -0
  53. package/lib/web/hooks/usePositionLabel.js +33 -0
  54. package/lib/web/hooks/usePreferencesObserver.d.ts +2 -0
  55. package/lib/web/hooks/usePreferencesObserver.js +54 -0
  56. package/lib/web/utils/manifestFetcher.d.ts +8 -0
  57. package/lib/web/utils/manifestFetcher.js +28 -0
  58. package/lib/web/utils/manifestNormalizer.d.ts +8 -0
  59. package/lib/web/utils/manifestNormalizer.js +70 -0
  60. package/lib/web/utils/metadataNormalizer.d.ts +53 -0
  61. package/lib/web/utils/metadataNormalizer.js +220 -0
  62. package/lib/web/utils/navigatorListeners.d.ts +6 -0
  63. package/lib/web/utils/navigatorListeners.js +50 -0
  64. package/lib/web/utils/publicationUtils.d.ts +15 -0
  65. package/lib/web/utils/publicationUtils.js +39 -0
  66. package/package.json +24 -14
  67. package/react-native-readium.podspec +7 -5
  68. package/src/ReadiumViewNativeComponent.ts +35 -0
  69. package/src/components/BaseReadiumView.tsx +3 -10
  70. package/src/components/ReadiumView.tsx +15 -15
  71. package/src/components/ReadiumView.web.tsx +120 -27
  72. package/src/interfaces/BaseReadiumViewProps.ts +2 -1
  73. package/src/interfaces/Preferences.ts +3 -2
  74. package/src/interfaces/PublicationMetadata.ts +141 -0
  75. package/src/interfaces/PublicationReady.ts +18 -0
  76. package/src/interfaces/index.ts +2 -0
  77. package/src/utils/index.ts +0 -1
  78. package/web/hooks/index.ts +3 -2
  79. package/web/hooks/useLocationObserver.ts +24 -11
  80. package/web/hooks/useNavigator.ts +146 -0
  81. package/web/hooks/usePositionLabel.ts +51 -0
  82. package/web/hooks/usePreferencesObserver.ts +69 -0
  83. package/web/utils/manifestFetcher.ts +38 -0
  84. package/web/utils/manifestNormalizer.ts +74 -0
  85. package/web/utils/metadataNormalizer.ts +238 -0
  86. package/web/utils/navigatorListeners.ts +60 -0
  87. package/web/utils/publicationUtils.ts +47 -0
  88. package/android/src/main/java/com/reactnativereadium/search/SearchFragment.kt +0 -100
  89. package/android/src/main/java/com/reactnativereadium/search/SearchPagingSource.kt +0 -44
  90. package/android/src/main/java/com/reactnativereadium/search/SearchResultAdapter.kt +0 -68
  91. package/android/src/main/java/com/reactnativereadium/utils/R2DispatcherActivity.kt +0 -45
  92. package/android/src/main/java/com/reactnativereadium/utils/SectionDecoration.kt +0 -98
  93. package/android/src/main/java/com/reactnativereadium/utils/SingleClickListener.kt +0 -32
  94. package/android/src/main/java/com/reactnativereadium/utils/extensions/Bitmap.kt +0 -23
  95. package/android/src/main/java/com/reactnativereadium/utils/extensions/Context.kt +0 -16
  96. package/android/src/main/java/com/reactnativereadium/utils/extensions/File.kt +0 -22
  97. package/android/src/main/java/com/reactnativereadium/utils/extensions/Link.kt +0 -6
  98. package/android/src/main/java/com/reactnativereadium/utils/extensions/Metadata.kt +0 -6
  99. package/android/src/main/java/com/reactnativereadium/utils/extensions/URL.kt +0 -29
  100. package/android/src/main/java/com/reactnativereadium/utils/extensions/Uri.kt +0 -17
  101. package/android/src/main/res/layout/fragment_search.xml +0 -39
  102. package/android/src/main/res/layout/item_recycle_search.xml +0 -14
  103. package/ios/Common/EPUBPreferences.swift +0 -8
  104. package/ios/Common/Paths.swift +0 -52
  105. package/ios/Common/Publication.swift +0 -15
  106. package/ios/Common/Toolkit/Extensions/HTTPClient.swift +0 -65
  107. package/ios/Common/Toolkit/Extensions/UIImage.swift +0 -12
  108. package/ios/Common/Toolkit/Extensions/UIViewController.swift +0 -19
  109. package/ios/Common/Toolkit/ScreenOrientation.swift +0 -13
  110. package/lib/src/utils/createFragment.d.ts +0 -1
  111. package/lib/src/utils/createFragment.js +0 -10
  112. package/lib/web/hooks/useReaderRef.d.ts +0 -3
  113. package/lib/web/hooks/useReaderRef.js +0 -85
  114. package/lib/web/hooks/useSettingsObserver.d.ts +0 -2
  115. package/lib/web/hooks/useSettingsObserver.js +0 -44
  116. package/src/utils/createFragment.ts +0 -15
  117. package/web/hooks/useReaderRef.ts +0 -109
  118. 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
+ }