@ankhorage/expo-runtime 3.1.1 → 3.2.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/CHANGELOG.md +12 -0
- package/README.md +2 -1
- package/dist/createExpoRuntimeRegistry.d.ts.map +1 -1
- package/dist/createExpoRuntimeRegistry.js +5 -0
- package/dist/createExpoRuntimeRegistry.js.map +1 -1
- package/dist/expoRuntimePlanningMetadata.d.ts +9 -1
- package/dist/expoRuntimePlanningMetadata.d.ts.map +1 -1
- package/dist/expoRuntimePlanningMetadata.js +8 -0
- package/dist/expoRuntimePlanningMetadata.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/reader/ExpoReaderSurfaceAdapter.d.ts +4 -0
- package/dist/reader/ExpoReaderSurfaceAdapter.d.ts.map +1 -0
- package/dist/reader/ExpoReaderSurfaceAdapter.js +102 -0
- package/dist/reader/ExpoReaderSurfaceAdapter.js.map +1 -0
- package/dist/reader/ReaderDocumentError.d.ts +6 -0
- package/dist/reader/ReaderDocumentError.d.ts.map +1 -0
- package/dist/reader/ReaderDocumentError.js +9 -0
- package/dist/reader/ReaderDocumentError.js.map +1 -0
- package/dist/reader/ReaderViewport.d.ts +4 -0
- package/dist/reader/ReaderViewport.d.ts.map +1 -0
- package/dist/reader/ReaderViewport.js +212 -0
- package/dist/reader/ReaderViewport.js.map +1 -0
- package/dist/reader/drivers/EpubReaderDriver.d.ts +20 -0
- package/dist/reader/drivers/EpubReaderDriver.d.ts.map +1 -0
- package/dist/reader/drivers/EpubReaderDriver.js +259 -0
- package/dist/reader/drivers/EpubReaderDriver.js.map +1 -0
- package/dist/reader/drivers/PdfReaderDriver.d.ts +18 -0
- package/dist/reader/drivers/PdfReaderDriver.d.ts.map +1 -0
- package/dist/reader/drivers/PdfReaderDriver.js +198 -0
- package/dist/reader/drivers/PdfReaderDriver.js.map +1 -0
- package/dist/reader/epub/EpubArchive.d.ts +11 -0
- package/dist/reader/epub/EpubArchive.d.ts.map +1 -0
- package/dist/reader/epub/EpubArchive.js +65 -0
- package/dist/reader/epub/EpubArchive.js.map +1 -0
- package/dist/reader/epub/EpubArchiveFetcher.d.ts +11 -0
- package/dist/reader/epub/EpubArchiveFetcher.d.ts.map +1 -0
- package/dist/reader/epub/EpubArchiveFetcher.js +44 -0
- package/dist/reader/epub/EpubArchiveFetcher.js.map +1 -0
- package/dist/reader/epub/mediaTypes.d.ts +3 -0
- package/dist/reader/epub/mediaTypes.d.ts.map +1 -0
- package/dist/reader/epub/mediaTypes.js +26 -0
- package/dist/reader/epub/mediaTypes.js.map +1 -0
- package/dist/reader/epub/parseEpubPackage.d.ts +22 -0
- package/dist/reader/epub/parseEpubPackage.d.ts.map +1 -0
- package/dist/reader/epub/parseEpubPackage.js +98 -0
- package/dist/reader/epub/parseEpubPackage.js.map +1 -0
- package/dist/reader/epub/path.d.ts +4 -0
- package/dist/reader/epub/path.d.ts.map +1 -0
- package/dist/reader/epub/path.js +28 -0
- package/dist/reader/epub/path.js.map +1 -0
- package/dist/reader/epub/sanitizeEpubDocument.d.ts +3 -0
- package/dist/reader/epub/sanitizeEpubDocument.d.ts.map +1 -0
- package/dist/reader/epub/sanitizeEpubDocument.js +90 -0
- package/dist/reader/epub/sanitizeEpubDocument.js.map +1 -0
- package/dist/reader/loadReaderSourceAsync.d.ts +2 -0
- package/dist/reader/loadReaderSourceAsync.d.ts.map +1 -0
- package/dist/reader/loadReaderSourceAsync.js +49 -0
- package/dist/reader/loadReaderSourceAsync.js.map +1 -0
- package/dist/reader/readerViewportPresentation.d.ts +4 -0
- package/dist/reader/readerViewportPresentation.d.ts.map +1 -0
- package/dist/reader/readerViewportPresentation.js +26 -0
- package/dist/reader/readerViewportPresentation.js.map +1 -0
- package/dist/reader/types.d.ts +57 -0
- package/dist/reader/types.d.ts.map +1 -0
- package/dist/reader/types.js +2 -0
- package/dist/reader/types.js.map +1 -0
- package/dist/reader/validateEpubArchiveEntriesAsync.d.ts +3 -0
- package/dist/reader/validateEpubArchiveEntriesAsync.d.ts.map +1 -0
- package/dist/reader/validateEpubArchiveEntriesAsync.js +48 -0
- package/dist/reader/validateEpubArchiveEntriesAsync.js.map +1 -0
- package/package.json +11 -4
- package/src/createExpoRuntimeRegistry.test.tsx +9 -1
- package/src/createExpoRuntimeRegistry.tsx +7 -1
- package/src/expoRuntimePlanningMetadata.ts +9 -1
- package/src/index.ts +1 -0
- package/src/reader/ExpoReaderSurfaceAdapter.tsx +170 -0
- package/src/reader/ReaderDocumentError.ts +11 -0
- package/src/reader/ReaderViewport.tsx +319 -0
- package/src/reader/drivers/EpubReaderDriver.ts +309 -0
- package/src/reader/drivers/PdfReaderDriver.ts +253 -0
- package/src/reader/epub/EpubArchive.ts +83 -0
- package/src/reader/epub/EpubArchiveFetcher.ts +58 -0
- package/src/reader/epub/mediaTypes.ts +16 -0
- package/src/reader/epub/parseEpubPackage.test.ts +116 -0
- package/src/reader/epub/parseEpubPackage.ts +152 -0
- package/src/reader/epub/path.test.ts +22 -0
- package/src/reader/epub/path.ts +26 -0
- package/src/reader/epub/sanitizeEpubDocument.ts +121 -0
- package/src/reader/loadReaderSourceAsync.test.ts +52 -0
- package/src/reader/loadReaderSourceAsync.ts +59 -0
- package/src/reader/readerViewportPresentation.ts +28 -0
- package/src/reader/rendererPackages.test.ts +3 -1
- package/src/reader/types.ts +61 -0
- package/src/reader/validateEpubArchiveEntriesAsync.test.ts +142 -0
- package/src/reader/validateEpubArchiveEntriesAsync.ts +73 -0
- package/src/resolveExpoRuntimePlan.test.ts +36 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readerViewportPresentation.js","sourceRoot":"","sources":["../../src/reader/readerViewportPresentation.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,aAAa,GAAwB;IAChD,SAAS,EAAE,KAAK;IAChB,aAAa,EAAE,KAAK;IACpB,IAAI,EAAE,CAAC;IACP,QAAQ,EAAE,CAAC;IACX,MAAM,EAAE,SAAS;CAClB,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;CAiB5B,CAAC","sourcesContent":["import type { ReaderViewportState } from './types';\n\nexport const LOADING_STATE: ReaderViewportState = {\n canGoNext: false,\n canGoPrevious: false,\n page: 1,\n progress: 0,\n status: 'loading',\n};\n\nexport const READER_STYLES = `\n :root, body, #root { height: 100%; margin: 0; overflow: hidden; }\n * { box-sizing: border-box; }\n .reader-root { background: #fff; color: #111; height: 100%; outline: none; overflow: hidden; touch-action: pan-y; width: 100%; }\n .reader-dark { background: #111; color: #f5f5f5; }\n .reader-sepia { background: #f4ecd8; color: #3f3527; }\n .reader-content { height: 100%; overflow: auto; position: relative; width: 100%; }\n .reader-content > div:not(.reader-pdf-page) { height: 100%; width: 100%; }\n .reader-pdf-page { --user-unit: 1; --total-scale-factor: calc(var(--scale-factor) * var(--user-unit)); margin: 0 auto; position: relative; }\n .reader-pdf-page canvas { display: block; position: absolute; inset: 0; }\n .reader-pdf-text-layer { --min-font-size: 1; --text-scale-factor: calc(var(--total-scale-factor) * var(--min-font-size)); --min-font-size-inv: calc(1 / var(--min-font-size)); inset: 0; line-height: 1; opacity: 1; overflow: hidden; position: absolute; text-align: initial; text-size-adjust: none; transform-origin: 0 0; }\n .reader-pdf-text-layer span, .reader-pdf-text-layer br { color: transparent; cursor: text; position: absolute; transform-origin: 0 0; user-select: text; white-space: pre; }\n .reader-pdf-text-layer > :not(.markedContent), .reader-pdf-text-layer .markedContent span:not(.markedContent) { --font-height: 0; --scale-x: 1; --rotate: 0deg; font-size: calc(var(--text-scale-factor) * var(--font-height)); transform: rotate(var(--rotate)) scaleX(var(--scale-x)) scale(var(--min-font-size-inv)); }\n .reader-pdf-text-layer .markedContent { display: contents; }\n .reader-pdf-text-layer ::selection { background: rgba(0, 100, 255, 0.35); }\n .reader-pdf-link-layer { inset: 0; pointer-events: none; position: absolute; }\n .reader-pdf-link-layer a { outline-offset: 2px; pointer-events: auto; position: absolute; }\n`;\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ReaderErrorEvent, ReaderLocationChangeEvent } from '@ankhorage/zora';
|
|
2
|
+
import type { DOMProps } from 'expo/dom';
|
|
3
|
+
export interface ReaderAppearance {
|
|
4
|
+
readonly colorScheme: 'dark' | 'light' | 'sepia' | 'system';
|
|
5
|
+
readonly fontScale: number;
|
|
6
|
+
readonly lineHeight: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ReaderCommand {
|
|
9
|
+
readonly id: number;
|
|
10
|
+
readonly sourceUri: string;
|
|
11
|
+
readonly type: 'next' | 'previous';
|
|
12
|
+
}
|
|
13
|
+
export interface ReaderViewportState {
|
|
14
|
+
readonly canGoNext: boolean;
|
|
15
|
+
readonly canGoPrevious: boolean;
|
|
16
|
+
readonly location?: ReaderLocationChangeEvent;
|
|
17
|
+
readonly page: number;
|
|
18
|
+
readonly pageCount?: number;
|
|
19
|
+
readonly progress: number;
|
|
20
|
+
readonly status: 'loading' | 'ready';
|
|
21
|
+
}
|
|
22
|
+
export interface ReaderViewportProps {
|
|
23
|
+
readonly appearance: ReaderAppearance;
|
|
24
|
+
readonly command?: ReaderCommand;
|
|
25
|
+
readonly format: 'epub' | 'pdf';
|
|
26
|
+
readonly initialLocation?: string;
|
|
27
|
+
readonly onError: (event: ReaderErrorEvent) => Promise<void>;
|
|
28
|
+
readonly onOpenExternalLink: (event: {
|
|
29
|
+
readonly url: string;
|
|
30
|
+
}) => Promise<void>;
|
|
31
|
+
readonly onStateChange: (state: ReaderViewportState) => Promise<void>;
|
|
32
|
+
readonly sourceUri: string;
|
|
33
|
+
readonly dom?: DOMProps;
|
|
34
|
+
}
|
|
35
|
+
export interface ReaderDriverState {
|
|
36
|
+
readonly canGoNext: boolean;
|
|
37
|
+
readonly canGoPrevious: boolean;
|
|
38
|
+
readonly chapterId?: string;
|
|
39
|
+
readonly chapterTitle?: string;
|
|
40
|
+
readonly locator: string;
|
|
41
|
+
readonly page: number;
|
|
42
|
+
readonly pageCount?: number;
|
|
43
|
+
readonly progression: number;
|
|
44
|
+
}
|
|
45
|
+
export interface ReaderDriver {
|
|
46
|
+
readonly direction: 'ltr' | 'rtl';
|
|
47
|
+
destroy(): Promise<void>;
|
|
48
|
+
getState(): ReaderDriverState;
|
|
49
|
+
goNext(trigger: ReaderLocationChangeEvent['trigger']): Promise<void>;
|
|
50
|
+
goPrevious(trigger: ReaderLocationChangeEvent['trigger']): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
export interface ReaderTouchStart {
|
|
53
|
+
readonly interactive: boolean;
|
|
54
|
+
readonly x: number;
|
|
55
|
+
readonly y: number;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/reader/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/reader/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ReaderErrorEvent, ReaderLocationChangeEvent } from '@ankhorage/zora';\nimport type { DOMProps } from 'expo/dom';\n\nexport interface ReaderAppearance {\n readonly colorScheme: 'dark' | 'light' | 'sepia' | 'system';\n readonly fontScale: number;\n readonly lineHeight: number;\n}\n\nexport interface ReaderCommand {\n readonly id: number;\n readonly sourceUri: string;\n readonly type: 'next' | 'previous';\n}\n\nexport interface ReaderViewportState {\n readonly canGoNext: boolean;\n readonly canGoPrevious: boolean;\n readonly location?: ReaderLocationChangeEvent;\n readonly page: number;\n readonly pageCount?: number;\n readonly progress: number;\n readonly status: 'loading' | 'ready';\n}\n\nexport interface ReaderViewportProps {\n readonly appearance: ReaderAppearance;\n readonly command?: ReaderCommand;\n readonly format: 'epub' | 'pdf';\n readonly initialLocation?: string;\n readonly onError: (event: ReaderErrorEvent) => Promise<void>;\n readonly onOpenExternalLink: (event: { readonly url: string }) => Promise<void>;\n readonly onStateChange: (state: ReaderViewportState) => Promise<void>;\n readonly sourceUri: string;\n readonly dom?: DOMProps;\n}\n\nexport interface ReaderDriverState {\n readonly canGoNext: boolean;\n readonly canGoPrevious: boolean;\n readonly chapterId?: string;\n readonly chapterTitle?: string;\n readonly locator: string;\n readonly page: number;\n readonly pageCount?: number;\n readonly progression: number;\n}\n\nexport interface ReaderDriver {\n readonly direction: 'ltr' | 'rtl';\n destroy(): Promise<void>;\n getState(): ReaderDriverState;\n goNext(trigger: ReaderLocationChangeEvent['trigger']): Promise<void>;\n goPrevious(trigger: ReaderLocationChangeEvent['trigger']): Promise<void>;\n}\n\nexport interface ReaderTouchStart {\n readonly interactive: boolean;\n readonly x: number;\n readonly y: number;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateEpubArchiveEntriesAsync.d.ts","sourceRoot":"","sources":["../../src/reader/validateEpubArchiveEntriesAsync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAQ5C,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,SAAS,KAAK,EAAE,EACzB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAkDf"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Uint8ArrayWriter } from '@zip.js/zip.js';
|
|
2
|
+
const MAX_COMPRESSION_RATIO = 100;
|
|
3
|
+
const MAX_ENTRY_COUNT = 10_000;
|
|
4
|
+
const MAX_ENTRY_UNCOMPRESSED_BYTES = 64 * 1024 * 1024;
|
|
5
|
+
const MAX_TOTAL_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
|
|
6
|
+
export async function validateEpubArchiveEntriesAsync(entries, signal) {
|
|
7
|
+
if (entries.length > MAX_ENTRY_COUNT) {
|
|
8
|
+
throw new Error(`EPUB archive exceeds the ${MAX_ENTRY_COUNT}-entry limit.`);
|
|
9
|
+
}
|
|
10
|
+
let totalUncompressedBytes = 0;
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
validateDeclaredSizes(entry);
|
|
13
|
+
if (entry.directory) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (entry.uncompressedSize > MAX_ENTRY_UNCOMPRESSED_BYTES) {
|
|
17
|
+
throw new Error(`EPUB archive entry "${entry.filename}" exceeds the ${MAX_ENTRY_UNCOMPRESSED_BYTES}-byte uncompressed limit.`);
|
|
18
|
+
}
|
|
19
|
+
if (entry.uncompressedSize > 0 &&
|
|
20
|
+
(entry.compressedSize === 0 ||
|
|
21
|
+
entry.uncompressedSize > entry.compressedSize * MAX_COMPRESSION_RATIO)) {
|
|
22
|
+
throw new Error(`EPUB archive entry "${entry.filename}" exceeds the ${MAX_COMPRESSION_RATIO}:1 compression-ratio limit.`);
|
|
23
|
+
}
|
|
24
|
+
if (totalUncompressedBytes > MAX_TOTAL_UNCOMPRESSED_BYTES - entry.uncompressedSize) {
|
|
25
|
+
throw new Error(`EPUB archive exceeds the ${MAX_TOTAL_UNCOMPRESSED_BYTES}-byte aggregate uncompressed limit.`);
|
|
26
|
+
}
|
|
27
|
+
totalUncompressedBytes += entry.uncompressedSize;
|
|
28
|
+
}
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (entry.directory) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
await entry.getData(new Uint8ArrayWriter(), {
|
|
34
|
+
checkOverlappingEntryOnly: true,
|
|
35
|
+
signal,
|
|
36
|
+
strictness: 'strict',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function validateDeclaredSizes(entry) {
|
|
41
|
+
if (!Number.isSafeInteger(entry.compressedSize) ||
|
|
42
|
+
entry.compressedSize < 0 ||
|
|
43
|
+
!Number.isSafeInteger(entry.uncompressedSize) ||
|
|
44
|
+
entry.uncompressedSize < 0) {
|
|
45
|
+
throw new Error(`EPUB archive entry "${entry.filename}" declares invalid sizes.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=validateEpubArchiveEntriesAsync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateEpubArchiveEntriesAsync.js","sourceRoot":"","sources":["../../src/reader/validateEpubArchiveEntriesAsync.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAClC,MAAM,eAAe,GAAG,MAAM,CAAC;AAC/B,MAAM,4BAA4B,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AACtD,MAAM,4BAA4B,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAEvD,MAAM,CAAC,KAAK,UAAU,+BAA+B,CACnD,OAAyB,EACzB,MAAoB;IAEpB,IAAI,OAAO,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,4BAA4B,eAAe,eAAe,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,sBAAsB,GAAG,CAAC,CAAC;IAE/B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,qBAAqB,CAAC,KAAK,CAAC,CAAC;QAE7B,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACpB,SAAS;QACX,CAAC;QAED,IAAI,KAAK,CAAC,gBAAgB,GAAG,4BAA4B,EAAE,CAAC;YAC1D,MAAM,IAAI,KAAK,CACb,uBAAuB,KAAK,CAAC,QAAQ,iBAAiB,4BAA4B,2BAA2B,CAC9G,CAAC;QACJ,CAAC;QAED,IACE,KAAK,CAAC,gBAAgB,GAAG,CAAC;YAC1B,CAAC,KAAK,CAAC,cAAc,KAAK,CAAC;gBACzB,KAAK,CAAC,gBAAgB,GAAG,KAAK,CAAC,cAAc,GAAG,qBAAqB,CAAC,EACxE,CAAC;YACD,MAAM,IAAI,KAAK,CACb,uBAAuB,KAAK,CAAC,QAAQ,iBAAiB,qBAAqB,6BAA6B,CACzG,CAAC;QACJ,CAAC;QAED,IAAI,sBAAsB,GAAG,4BAA4B,GAAG,KAAK,CAAC,gBAAgB,EAAE,CAAC;YACnF,MAAM,IAAI,KAAK,CACb,4BAA4B,4BAA4B,qCAAqC,CAC9F,CAAC;QACJ,CAAC;QAED,sBAAsB,IAAI,KAAK,CAAC,gBAAgB,CAAC;IACnD,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACpB,SAAS;QACX,CAAC;QAED,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,gBAAgB,EAAE,EAAE;YAC1C,yBAAyB,EAAE,IAAI;YAC/B,MAAM;YACN,UAAU,EAAE,QAAQ;SACrB,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAY;IACzC,IACE,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,cAAc,CAAC;QAC3C,KAAK,CAAC,cAAc,GAAG,CAAC;QACxB,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,gBAAgB,CAAC;QAC7C,KAAK,CAAC,gBAAgB,GAAG,CAAC,EAC1B,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,uBAAuB,KAAK,CAAC,QAAQ,2BAA2B,CAAC,CAAC;IACpF,CAAC;AACH,CAAC","sourcesContent":["import type { Entry } from '@zip.js/zip.js';\nimport { Uint8ArrayWriter } from '@zip.js/zip.js';\n\nconst MAX_COMPRESSION_RATIO = 100;\nconst MAX_ENTRY_COUNT = 10_000;\nconst MAX_ENTRY_UNCOMPRESSED_BYTES = 64 * 1024 * 1024;\nconst MAX_TOTAL_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;\n\nexport async function validateEpubArchiveEntriesAsync(\n entries: readonly Entry[],\n signal?: AbortSignal,\n): Promise<void> {\n if (entries.length > MAX_ENTRY_COUNT) {\n throw new Error(`EPUB archive exceeds the ${MAX_ENTRY_COUNT}-entry limit.`);\n }\n\n let totalUncompressedBytes = 0;\n\n for (const entry of entries) {\n validateDeclaredSizes(entry);\n\n if (entry.directory) {\n continue;\n }\n\n if (entry.uncompressedSize > MAX_ENTRY_UNCOMPRESSED_BYTES) {\n throw new Error(\n `EPUB archive entry \"${entry.filename}\" exceeds the ${MAX_ENTRY_UNCOMPRESSED_BYTES}-byte uncompressed limit.`,\n );\n }\n\n if (\n entry.uncompressedSize > 0 &&\n (entry.compressedSize === 0 ||\n entry.uncompressedSize > entry.compressedSize * MAX_COMPRESSION_RATIO)\n ) {\n throw new Error(\n `EPUB archive entry \"${entry.filename}\" exceeds the ${MAX_COMPRESSION_RATIO}:1 compression-ratio limit.`,\n );\n }\n\n if (totalUncompressedBytes > MAX_TOTAL_UNCOMPRESSED_BYTES - entry.uncompressedSize) {\n throw new Error(\n `EPUB archive exceeds the ${MAX_TOTAL_UNCOMPRESSED_BYTES}-byte aggregate uncompressed limit.`,\n );\n }\n\n totalUncompressedBytes += entry.uncompressedSize;\n }\n\n for (const entry of entries) {\n if (entry.directory) {\n continue;\n }\n\n await entry.getData(new Uint8ArrayWriter(), {\n checkOverlappingEntryOnly: true,\n signal,\n strictness: 'strict',\n });\n }\n}\n\nfunction validateDeclaredSizes(entry: Entry): void {\n if (\n !Number.isSafeInteger(entry.compressedSize) ||\n entry.compressedSize < 0 ||\n !Number.isSafeInteger(entry.uncompressedSize) ||\n entry.uncompressedSize < 0\n ) {\n throw new Error(`EPUB archive entry \"${entry.filename}\" declares invalid sizes.`);\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/expo-runtime",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.2.0",
|
|
5
5
|
"description": "Declarative runtime integration for Expo apps: maps app capabilities to permissions, packages, config plugins, providers, and adapters",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/expo-runtime#readme",
|
|
7
7
|
"bugs": {
|
|
@@ -121,13 +121,15 @@
|
|
|
121
121
|
"validate:expo-platform": "bun scripts/validateExpoPlatform.ts",
|
|
122
122
|
"validate:headless-platform": "bun scripts/headlessPlatformAcceptance.ts",
|
|
123
123
|
"validate:packed-runtime": "bun scripts/packedRuntimeAcceptance.ts",
|
|
124
|
+
"validate:reader-dom": "bun scripts/readerDomAcceptance.ts",
|
|
124
125
|
"validate:web-barcode": "bun scripts/webBarcodeAcceptance.ts",
|
|
125
126
|
"version-packages": "ankhorage-changeset version"
|
|
126
127
|
},
|
|
127
128
|
"peerDependencies": {
|
|
128
129
|
"@ankhorage/permissions": "^0.2.3",
|
|
129
|
-
"@ankhorage/zora": "^3.0
|
|
130
|
+
"@ankhorage/zora": "^3.1.0",
|
|
130
131
|
"@readium/navigator": "2.8.2",
|
|
132
|
+
"@readium/shared": "2.4.0",
|
|
131
133
|
"@react-native-vector-icons/fontawesome": "^13.1.3",
|
|
132
134
|
"@react-native-vector-icons/fontawesome5": "^13.1.3",
|
|
133
135
|
"@react-native-vector-icons/fontawesome6": "^13.1.3",
|
|
@@ -155,6 +157,9 @@
|
|
|
155
157
|
"@readium/navigator": {
|
|
156
158
|
"optional": true
|
|
157
159
|
},
|
|
160
|
+
"@readium/shared": {
|
|
161
|
+
"optional": true
|
|
162
|
+
},
|
|
158
163
|
"@react-native-vector-icons/fontawesome": {
|
|
159
164
|
"optional": true
|
|
160
165
|
},
|
|
@@ -205,11 +210,12 @@
|
|
|
205
210
|
}
|
|
206
211
|
},
|
|
207
212
|
"devDependencies": {
|
|
208
|
-
"@ankhorage/devtools": "^1.8.
|
|
213
|
+
"@ankhorage/devtools": "^1.8.3",
|
|
209
214
|
"@ankhorage/paradox": "^0.1.24",
|
|
210
215
|
"@ankhorage/permissions": "^0.2.3",
|
|
211
|
-
"@ankhorage/zora": "^3.0
|
|
216
|
+
"@ankhorage/zora": "^3.1.0",
|
|
212
217
|
"@readium/navigator": "2.8.2",
|
|
218
|
+
"@readium/shared": "2.4.0",
|
|
213
219
|
"@react-native-picker/picker": "2.11.4",
|
|
214
220
|
"@react-native-vector-icons/fontawesome": "^13.1.3",
|
|
215
221
|
"@react-native-vector-icons/fontawesome5": "^13.1.3",
|
|
@@ -221,6 +227,7 @@
|
|
|
221
227
|
"@types/node": "^24.13.3",
|
|
222
228
|
"@types/pngjs": "^6.0.5",
|
|
223
229
|
"@types/react": "^19.2.18",
|
|
230
|
+
"@xmldom/xmldom": "0.9.10",
|
|
224
231
|
"bwip-js": "^4.11.4",
|
|
225
232
|
"expo": "57.0.18",
|
|
226
233
|
"expo-camera": "~57.0.4",
|
|
@@ -4,20 +4,26 @@ import React from 'react';
|
|
|
4
4
|
const BaseBarcodeScannerView = (): React.ReactElement => React.createElement(React.Fragment);
|
|
5
5
|
const BaseText = (): React.ReactElement => React.createElement(React.Fragment);
|
|
6
6
|
const MockExpoBarcodeScannerAdapter = (): React.ReactElement => React.createElement(React.Fragment);
|
|
7
|
+
const BaseReaderSurface = (): React.ReactElement => React.createElement(React.Fragment);
|
|
8
|
+
const MockExpoReaderSurfaceAdapter = (): React.ReactElement => React.createElement(React.Fragment);
|
|
7
9
|
|
|
8
10
|
describe('createExpoRuntimeRegistry', () => {
|
|
9
11
|
afterEach(() => {
|
|
10
12
|
mock.restore();
|
|
11
13
|
});
|
|
12
14
|
|
|
13
|
-
it('overrides
|
|
15
|
+
it('overrides platform-owned views while preserving unrelated registry entries', async () => {
|
|
14
16
|
void mock.module('./ExpoBarcodeScannerAdapter', () => ({
|
|
15
17
|
ExpoBarcodeScannerAdapter: MockExpoBarcodeScannerAdapter,
|
|
16
18
|
}));
|
|
19
|
+
void mock.module('./reader/ExpoReaderSurfaceAdapter', () => ({
|
|
20
|
+
ExpoReaderSurfaceAdapter: MockExpoReaderSurfaceAdapter,
|
|
21
|
+
}));
|
|
17
22
|
|
|
18
23
|
const { createExpoRuntimeRegistry } = await import('./createExpoRuntimeRegistry');
|
|
19
24
|
const baseRegistry = {
|
|
20
25
|
BarcodeScannerView: BaseBarcodeScannerView,
|
|
26
|
+
ReaderSurface: BaseReaderSurface,
|
|
21
27
|
Text: BaseText,
|
|
22
28
|
};
|
|
23
29
|
|
|
@@ -27,5 +33,7 @@ describe('createExpoRuntimeRegistry', () => {
|
|
|
27
33
|
expect(registry.Text).toBe(BaseText);
|
|
28
34
|
expect(registry.BarcodeScannerView).not.toBe(BaseBarcodeScannerView);
|
|
29
35
|
expect(typeof registry.BarcodeScannerView).toBe('function');
|
|
36
|
+
expect(registry.ReaderSurface).not.toBe(BaseReaderSurface);
|
|
37
|
+
expect(typeof registry.ReaderSurface).toBe('function');
|
|
30
38
|
});
|
|
31
39
|
});
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { type BarcodeScannerViewProps } from '@ankhorage/zora';
|
|
1
|
+
import { type BarcodeScannerViewProps, type ReaderSurfaceProps } from '@ankhorage/zora';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
|
|
4
4
|
import { type ComponentRegistry, createComponentRegistry } from './componentRegistry';
|
|
5
5
|
import { ExpoBarcodeScannerAdapter } from './ExpoBarcodeScannerAdapter';
|
|
6
|
+
import { ExpoReaderSurfaceAdapter } from './reader/ExpoReaderSurfaceAdapter';
|
|
6
7
|
|
|
7
8
|
const EXPO_RUNTIME_OVERRIDES: ComponentRegistry = {
|
|
8
9
|
BarcodeScannerView: ExpoBarcodeScannerView,
|
|
10
|
+
ReaderSurface: ExpoReaderSurface,
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
export function createExpoRuntimeRegistry(baseRegistry: ComponentRegistry): ComponentRegistry {
|
|
@@ -15,3 +17,7 @@ export function createExpoRuntimeRegistry(baseRegistry: ComponentRegistry): Comp
|
|
|
15
17
|
function ExpoBarcodeScannerView(props: BarcodeScannerViewProps) {
|
|
16
18
|
return <ExpoBarcodeScannerAdapter {...props} />;
|
|
17
19
|
}
|
|
20
|
+
|
|
21
|
+
function ExpoReaderSurface(props: ReaderSurfaceProps) {
|
|
22
|
+
return <ExpoReaderSurfaceAdapter {...props} />;
|
|
23
|
+
}
|
|
@@ -13,7 +13,7 @@ export interface ExpoRuntimeConfigPlugin {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export type ExpoRuntimeProviderId = 'permissions';
|
|
16
|
-
export type ExpoRuntimeAdapterId = 'ExpoBarcodeScannerAdapter';
|
|
16
|
+
export type ExpoRuntimeAdapterId = 'ExpoBarcodeScannerAdapter' | 'ExpoReaderSurfaceAdapter';
|
|
17
17
|
|
|
18
18
|
export interface ExpoRuntimeDiagnostic {
|
|
19
19
|
readonly severity: 'error' | 'warning';
|
|
@@ -40,6 +40,10 @@ export interface ExpoRuntimeHintMetadata {
|
|
|
40
40
|
const GENERATED_ANKHORAGE_DEPENDENCY_VERSIONS = {
|
|
41
41
|
'@ankhorage/permissions': '^0.2.3',
|
|
42
42
|
'@ankhorage/expo-runtime': '^3.0.0',
|
|
43
|
+
'@readium/navigator': '2.8.2',
|
|
44
|
+
'@readium/shared': '2.4.0',
|
|
45
|
+
'@zip.js/zip.js': '2.9.0',
|
|
46
|
+
'pdfjs-dist': '6.3.289',
|
|
43
47
|
} as const satisfies Record<string, string>;
|
|
44
48
|
|
|
45
49
|
const GENERATED_EXPO_DEPENDENCY_VERSIONS = Object.fromEntries(
|
|
@@ -148,6 +152,10 @@ export const EXPO_CAPABILITY_RUNTIME_REGISTRY = {
|
|
|
148
152
|
},
|
|
149
153
|
],
|
|
150
154
|
},
|
|
155
|
+
ebookReader: {
|
|
156
|
+
requiredPackages: ['@readium/navigator', '@readium/shared', '@zip.js/zip.js', 'pdfjs-dist'],
|
|
157
|
+
runtimeAdapters: ['ExpoReaderSurfaceAdapter'],
|
|
158
|
+
},
|
|
151
159
|
} as const satisfies Readonly<
|
|
152
160
|
Partial<Record<AnkhorageCapabilityName, ExpoRuntimeCapabilityMetadata>>
|
|
153
161
|
>;
|
package/src/index.ts
CHANGED
|
@@ -53,6 +53,7 @@ export {
|
|
|
53
53
|
type ExpoRuntimeLayoutIntegrationPlan,
|
|
54
54
|
resolveExpoRuntimeLayoutIntegration,
|
|
55
55
|
} from './layoutIntegrationPlanning';
|
|
56
|
+
export { ExpoReaderSurfaceAdapter } from './reader/ExpoReaderSurfaceAdapter';
|
|
56
57
|
export type {
|
|
57
58
|
ExpoRuntimeAdapterId,
|
|
58
59
|
ExpoRuntimePlan,
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { type ReaderErrorEvent, ReaderSurface, type ReaderSurfaceProps } from '@ankhorage/zora';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Image, StyleSheet, View } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import ReaderViewport from './ReaderViewport';
|
|
6
|
+
import type { ReaderAppearance, ReaderCommand, ReaderViewportState } from './types';
|
|
7
|
+
|
|
8
|
+
const INITIAL_STATE: ReaderViewportState = {
|
|
9
|
+
canGoNext: false,
|
|
10
|
+
canGoPrevious: false,
|
|
11
|
+
page: 1,
|
|
12
|
+
progress: 0,
|
|
13
|
+
status: 'loading',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function ExpoReaderSurfaceAdapter(props: ReaderSurfaceProps) {
|
|
17
|
+
const sourceUri = resolveReaderSourceUri(props.source);
|
|
18
|
+
const controller = useReaderController(props, sourceUri);
|
|
19
|
+
const appearance = useReaderAppearance(props);
|
|
20
|
+
const viewport = sourceUri ? (
|
|
21
|
+
<ReaderAdapterViewport
|
|
22
|
+
appearance={appearance}
|
|
23
|
+
command={controller.command}
|
|
24
|
+
format={props.format}
|
|
25
|
+
initialLocation={props.location}
|
|
26
|
+
onError={controller.handleError}
|
|
27
|
+
onOpenExternalLink={controller.handleOpenExternalLink}
|
|
28
|
+
onStateChange={controller.handleStateChange}
|
|
29
|
+
sourceUri={sourceUri}
|
|
30
|
+
/>
|
|
31
|
+
) : undefined;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<ReaderSurface
|
|
35
|
+
{...props}
|
|
36
|
+
canGoNext={controller.state.canGoNext}
|
|
37
|
+
canGoPrevious={controller.state.canGoPrevious}
|
|
38
|
+
chapterLabel={controller.state.location?.chapterTitle ?? props.chapterLabel}
|
|
39
|
+
onNextPage={controller.handleNextPage}
|
|
40
|
+
onPreviousPage={controller.handlePreviousPage}
|
|
41
|
+
page={controller.state.page}
|
|
42
|
+
pageCount={controller.state.pageCount}
|
|
43
|
+
progress={controller.state.progress}
|
|
44
|
+
status={controller.error ? 'error' : sourceUri ? controller.state.status : 'idle'}
|
|
45
|
+
viewport={viewport}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ReaderAdapterViewport({
|
|
51
|
+
appearance,
|
|
52
|
+
command,
|
|
53
|
+
format,
|
|
54
|
+
initialLocation,
|
|
55
|
+
onError,
|
|
56
|
+
onOpenExternalLink,
|
|
57
|
+
onStateChange,
|
|
58
|
+
sourceUri,
|
|
59
|
+
}: React.ComponentProps<typeof ReaderViewport>) {
|
|
60
|
+
return (
|
|
61
|
+
<View style={styles.viewport}>
|
|
62
|
+
<ReaderViewport
|
|
63
|
+
appearance={appearance}
|
|
64
|
+
command={command}
|
|
65
|
+
dom={DOM_CONFIGURATION}
|
|
66
|
+
format={format}
|
|
67
|
+
initialLocation={initialLocation}
|
|
68
|
+
key={`${format}:${sourceUri}`}
|
|
69
|
+
onError={onError}
|
|
70
|
+
onOpenExternalLink={onOpenExternalLink}
|
|
71
|
+
onStateChange={onStateChange}
|
|
72
|
+
sourceUri={sourceUri}
|
|
73
|
+
/>
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function useReaderController(props: ReaderSurfaceProps, sourceUri: string | undefined) {
|
|
79
|
+
const { onLocationChange, onNextPage, onOpenExternalLink, onPreviousPage, onReaderError } = props;
|
|
80
|
+
const [state, setState] = React.useState<ReaderViewportState>(INITIAL_STATE);
|
|
81
|
+
const [error, setError] = React.useState<ReaderErrorEvent>();
|
|
82
|
+
const { command, issueCommand } = useReaderCommandState(sourceUri);
|
|
83
|
+
const handlePreviousPage = React.useCallback(() => {
|
|
84
|
+
issueCommand('previous');
|
|
85
|
+
void onPreviousPage?.();
|
|
86
|
+
}, [issueCommand, onPreviousPage]);
|
|
87
|
+
const handleNextPage = React.useCallback(() => {
|
|
88
|
+
issueCommand('next');
|
|
89
|
+
void onNextPage?.();
|
|
90
|
+
}, [issueCommand, onNextPage]);
|
|
91
|
+
const handleStateChange = React.useCallback(
|
|
92
|
+
async (nextState: ReaderViewportState) => {
|
|
93
|
+
setState(nextState);
|
|
94
|
+
setError(undefined);
|
|
95
|
+
if (nextState.location) await onLocationChange?.(nextState.location);
|
|
96
|
+
},
|
|
97
|
+
[onLocationChange],
|
|
98
|
+
);
|
|
99
|
+
const handleError = React.useCallback(
|
|
100
|
+
async (event: ReaderErrorEvent) => {
|
|
101
|
+
setError(event);
|
|
102
|
+
await onReaderError?.(event);
|
|
103
|
+
},
|
|
104
|
+
[onReaderError],
|
|
105
|
+
);
|
|
106
|
+
const handleOpenExternalLink = React.useCallback(
|
|
107
|
+
async (event: { readonly url: string }) => onOpenExternalLink?.(event),
|
|
108
|
+
[onOpenExternalLink],
|
|
109
|
+
);
|
|
110
|
+
return {
|
|
111
|
+
command,
|
|
112
|
+
error,
|
|
113
|
+
handleError,
|
|
114
|
+
handleNextPage,
|
|
115
|
+
handleOpenExternalLink,
|
|
116
|
+
handlePreviousPage,
|
|
117
|
+
handleStateChange,
|
|
118
|
+
state,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function useReaderCommandState(sourceUri: string | undefined) {
|
|
123
|
+
const [command, setCommand] = React.useState<ReaderCommand>();
|
|
124
|
+
const commandIdRef = React.useRef(0);
|
|
125
|
+
const issueCommand = React.useCallback(
|
|
126
|
+
(type: ReaderCommand['type']) => {
|
|
127
|
+
if (!sourceUri) return;
|
|
128
|
+
commandIdRef.current += 1;
|
|
129
|
+
setCommand({ id: commandIdRef.current, sourceUri, type });
|
|
130
|
+
},
|
|
131
|
+
[sourceUri],
|
|
132
|
+
);
|
|
133
|
+
return { command, issueCommand };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function useReaderAppearance(props: ReaderSurfaceProps): ReaderAppearance {
|
|
137
|
+
const colorScheme = props.readerColorScheme ?? 'system';
|
|
138
|
+
const fontScale = props.fontScale ?? 1;
|
|
139
|
+
const lineHeight = resolveLineHeight(props.lineHeight ?? 'normal');
|
|
140
|
+
return React.useMemo(
|
|
141
|
+
() => ({ colorScheme, fontScale, lineHeight }),
|
|
142
|
+
[colorScheme, fontScale, lineHeight],
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveReaderSourceUri(source: ReaderSurfaceProps['source']): string | undefined {
|
|
147
|
+
if (typeof source === 'string') return source;
|
|
148
|
+
if (typeof source === 'number') return Image.resolveAssetSource(source).uri;
|
|
149
|
+
return source ? source.uri : undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resolveLineHeight(lineHeight: NonNullable<ReaderSurfaceProps['lineHeight']>): number {
|
|
153
|
+
if (lineHeight === 'compact') return 1.25;
|
|
154
|
+
if (lineHeight === 'relaxed') return 1.75;
|
|
155
|
+
return 1.5;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const styles = StyleSheet.create({
|
|
159
|
+
viewport: { flex: 1, minHeight: 360 },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const DOM_CONFIGURATION = {
|
|
163
|
+
allowsInlineMediaPlayback: false,
|
|
164
|
+
javaScriptCanOpenWindowsAutomatically: false,
|
|
165
|
+
mediaPlaybackRequiresUserAction: true,
|
|
166
|
+
originWhitelist: ['about:blank', 'blob:*'],
|
|
167
|
+
scrollEnabled: false,
|
|
168
|
+
style: styles.viewport,
|
|
169
|
+
unstable_useExpoModulesBridge: false,
|
|
170
|
+
};
|