@quran.ws/qvp-react-native 0.1.0 → 0.3.1
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/LICENSE +172 -0
- package/README.md +30 -28
- package/android/build.gradle +2 -2
- package/android/src/main/java/{net/quranpedia → ws/quran}/qvp/rn/Marshal.kt +33 -32
- package/android/src/main/java/{net/quranpedia → ws/quran}/qvp/rn/QvpModule.kt +55 -35
- package/android/src/main/java/{net/quranpedia → ws/quran}/qvp/rn/QvpPackage.kt +1 -1
- package/android/src/main/java/{net/quranpedia → ws/quran}/qvp/rn/QvpPageViewManager.kt +9 -6
- package/android/src/main/java/{net/quranpedia → ws/quran}/qvp/rn/QvpRnPageView.kt +54 -28
- package/package.json +1 -1
- package/src/index.tsx +147 -104
package/src/index.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @quran.ws/qvp-react-native — typed JS API over the native module.
|
|
3
3
|
*
|
|
4
4
|
* Names follow docs/API.md. Nothing here decides anything: targets, selectors and colours are marshalled
|
|
5
|
-
* to the Kotlin library (`
|
|
5
|
+
* to the Kotlin library (`ws.quran.qvp`), which is JNI over crates/qvp-ffi/include/qvp.h.
|
|
6
6
|
* Declarative props (`highlights`, `styles`, `theme`, `mask`, `reveal`) are reconciled against engine
|
|
7
7
|
* handles on the native side, so JS never sees a handle.
|
|
8
8
|
*/
|
|
@@ -20,20 +20,9 @@ import {
|
|
|
20
20
|
export const KIND = { BODY: 0, MARK: 1, AYAH_NUMBER: 2, AYAH_MARK_ORNAMENT: 3, HEADER_INK: 4, OTHER: 255 } as const;
|
|
21
21
|
export const FAMILY = { NONE: 0, DIACRITIC: 1, TANWIN: 2, DOTS: 3, WAQF: 4, SIFR: 5, SAJDAH: 6, READING_SIGN: 7 } as const;
|
|
22
22
|
export const CATEGORY = { NONE: 0, HARAKAH: 1, TANWIN: 2, LETTER_DOT: 3, ORTHOGRAPHIC: 4, DABT: 5, WAQF: 6, READING_SIGN: 7, STANDALONE: 8 } as const;
|
|
23
|
-
export const
|
|
23
|
+
export const DECORATION = { AYAH_MARK: 0, SURAH_NAME: 1, BASMALAH: 2, DIVISION_MARK: 3, SAJDAH_MARK: 4 } as const;
|
|
24
24
|
export const LAYER = { BASE: 0, THEME: 10, HIGHLIGHT: 50, SELECTION: 60, TOP: 100 } as const;
|
|
25
25
|
export const DIVISION = { JUZ: 0, HIZB: 1, NISF: 2, RUBU_AL_HIZB: 3 } as const;
|
|
26
|
-
export const MARKS = ['', 'fathah', 'kasrah', 'dammah', 'tanwin_al_fath', 'tanwin_al_kasr', 'tanwin_al_damm', 'shaddah', 'sukun', 'maddah', 'hamzah', 'hamzat_al_wasl', 'omitted_alif', 'small_waw', 'small_yaa', 'small_noon', 'dot', 'two_dots', 'three_dots', 'rounded_zero', 'rectangular_zero', 'waqf_jaiz_mustawi_al_tarafayn', 'waqf_jaiz_waqf_awla', 'waqf_jaiz_wasl_awla', 'waqf_lazim', 'waqf_al_muanaqah', 'saktah', 'small_meem', 'hizb', 'sajdah', 'sajdah_mark', 'sajdah_line', 'seen_al_qiraah', 'tashil', 'ishmam', 'imalah'];
|
|
27
|
-
const KIND_NAMES = ['body', 'mark', 'ayah-number', 'ayah-ornament', 'header-ink'];
|
|
28
|
-
const FAMILY_NAMES = ['none', 'diacritic', 'tanwin', 'dots', 'waqf', 'sifr', 'sajdah', 'reading_sign'];
|
|
29
|
-
const CATEGORY_NAMES = ['none', 'harakah', 'tanwin', 'letter_dot', 'orthographic', 'dabt', 'waqf', 'reading_sign', 'standalone'];
|
|
30
|
-
const DECO_NAMES = ['ayah-mark', 'surah-name', 'basmalah', 'division-mark', 'sajdah-mark', 'page-number', 'running-head'];
|
|
31
|
-
export function markId(name: string | number): number {
|
|
32
|
-
if (typeof name === 'number') return name;
|
|
33
|
-
const i = MARKS.indexOf(name);
|
|
34
|
-
return i < 0 ? 255 : i;
|
|
35
|
-
}
|
|
36
|
-
const named = (v: string | number, names: string[]) => (typeof v === 'number' ? v : Math.max(0, names.indexOf(v)));
|
|
37
26
|
|
|
38
27
|
// ── colours ──────────────────────────────────────────────────────────────────────────────────
|
|
39
28
|
export type Color = string | number;
|
|
@@ -53,38 +42,38 @@ export function rgba(c: Color, alpha: number): string {
|
|
|
53
42
|
}
|
|
54
43
|
|
|
55
44
|
// ── targets and selectors (same shapes as web/qvp.js T / Sel) ────────────────────────────────
|
|
56
|
-
export interface TargetObj {
|
|
45
|
+
export interface TargetObj { target: number; a?: number; b?: number; c?: number; words?: number[] }
|
|
57
46
|
/** 'page' | '2:255' | '2:255:3' | '2:255-257' | 'line:7' | 'surah:2' | word index | [indices] | T.* */
|
|
58
47
|
export type Target = string | number | number[] | TargetObj;
|
|
59
48
|
export const T = {
|
|
60
|
-
page: (): TargetObj => ({
|
|
61
|
-
word: (i: number): TargetObj => ({
|
|
62
|
-
words: (ws: number[]): TargetObj => ({
|
|
63
|
-
ayah: (s: number, a: number): TargetObj => ({
|
|
64
|
-
ayahRange: (s: number, a: number, b: number): TargetObj => ({
|
|
65
|
-
line: (n: number): TargetObj => ({
|
|
66
|
-
surah: (s: number): TargetObj => ({
|
|
67
|
-
range: (a: number, b: number): TargetObj => ({
|
|
49
|
+
page: (): TargetObj => ({ target: 0 }),
|
|
50
|
+
word: (i: number): TargetObj => ({ target: 1, a: i }),
|
|
51
|
+
words: (ws: number[]): TargetObj => ({ target: 2, words: ws }),
|
|
52
|
+
ayah: (s: number, a: number): TargetObj => ({ target: 3, a: s, b: a }),
|
|
53
|
+
ayahRange: (s: number, a: number, b: number): TargetObj => ({ target: 4, a: s, b: a, c: b }),
|
|
54
|
+
line: (n: number): TargetObj => ({ target: 5, a: n }),
|
|
55
|
+
surah: (s: number): TargetObj => ({ target: 6, a: s }),
|
|
56
|
+
range: (a: number, b: number): TargetObj => ({ target: 7, a, b }),
|
|
68
57
|
};
|
|
69
|
-
export interface Selector {
|
|
58
|
+
export interface Selector { selector: number; a?: number; b?: number; c?: number; mark?: string }
|
|
70
59
|
export const Sel = {
|
|
71
|
-
page: (): Selector => ({
|
|
72
|
-
path: (p: number): Selector => ({
|
|
73
|
-
wordPath: (w: number, nth: number): Selector => ({
|
|
60
|
+
page: (): Selector => ({ selector: 0 }),
|
|
61
|
+
path: (p: number): Selector => ({ selector: 1, a: p }),
|
|
62
|
+
wordPath: (w: number, nth: number): Selector => ({ selector: 2, a: w, b: nth }),
|
|
74
63
|
/** nth mark of the word (0-based, marks only) */
|
|
75
|
-
wordMark: (w: number, nth: number): Selector => ({
|
|
76
|
-
wordMarkNamed: (w: number, mark: string, nth = 0): Selector => ({
|
|
77
|
-
wordBody: (w: number): Selector => ({
|
|
78
|
-
wordMarks: (w: number): Selector => ({
|
|
79
|
-
word: (w: number): Selector => ({
|
|
80
|
-
ayah: (s: number, a: number): Selector => ({
|
|
81
|
-
line: (n: number): Selector => ({
|
|
82
|
-
mark: (m: string | number): Selector => ({
|
|
83
|
-
category: (c: string | number): Selector => ({
|
|
84
|
-
family: (f: string | number): Selector => ({
|
|
85
|
-
kind: (k: string | number): Selector => ({
|
|
86
|
-
|
|
87
|
-
|
|
64
|
+
wordMark: (w: number, nth: number): Selector => ({ selector: 3, a: w, b: nth }),
|
|
65
|
+
wordMarkNamed: (w: number, mark: string, nth = 0): Selector => ({ selector: 4, a: w, b: markId(mark), c: nth, mark }),
|
|
66
|
+
wordBody: (w: number): Selector => ({ selector: 5, a: w }),
|
|
67
|
+
wordMarks: (w: number): Selector => ({ selector: 6, a: w }),
|
|
68
|
+
word: (w: number): Selector => ({ selector: 7, a: w }),
|
|
69
|
+
ayah: (s: number, a: number): Selector => ({ selector: 8, a: s, b: a }),
|
|
70
|
+
line: (n: number): Selector => ({ selector: 9, a: n }),
|
|
71
|
+
mark: (m: string | number): Selector => ({ selector: 10, a: markId(m) }),
|
|
72
|
+
category: (c: string | number): Selector => ({ selector: 11, a: named(c, CATEGORY_NAMES) }),
|
|
73
|
+
family: (f: string | number): Selector => ({ selector: 12, a: named(f, FAMILY_NAMES) }),
|
|
74
|
+
kind: (k: string | number): Selector => ({ selector: 13, a: named(k, KIND_NAMES) }),
|
|
75
|
+
decoration: (k: string | number): Selector => ({ selector: 14, a: named(k, DECORATION_NAMES) }),
|
|
76
|
+
decorationIndex: (d: number): Selector => ({ selector: 15, a: d }),
|
|
88
77
|
};
|
|
89
78
|
|
|
90
79
|
// ── records ──────────────────────────────────────────────────────────────────────────────────
|
|
@@ -92,41 +81,49 @@ export type Form = 'rasmUthmani' | 'rasmImlai' | 'qpc' | 'rasm' | 'search';
|
|
|
92
81
|
export type HighlightMode = 'ink' | 'band' | 'both';
|
|
93
82
|
export type MaskMode = 'hide' | 'block' | 'blur';
|
|
94
83
|
export type DivisionKind = 'juz' | 'hizb' | 'nisf' | 'rubuAlHizb';
|
|
95
|
-
export interface WordPath {
|
|
84
|
+
export interface WordPath { index: number; kind: number; kindName: string; mark: number; markName: string; nthMark: number; nthInWord: number; category: number; categoryName: string; family: number; familyName: string; line: number }
|
|
96
85
|
export interface Word {
|
|
97
|
-
|
|
86
|
+
index: number; surah: number; ayah: number; word: number; line: number; lineIndex: number; ayahIndex: number;
|
|
98
87
|
x0: number; y0: number; x1: number; y1: number; text: string; firstPath: number; nPaths: number;
|
|
99
88
|
wordKey: string; ayahKey: string; forms: Partial<Record<Form, string>>; label: string; paths: WordPath[];
|
|
100
89
|
}
|
|
101
|
-
export interface Deco {
|
|
102
|
-
|
|
103
|
-
export interface
|
|
104
|
-
export interface
|
|
90
|
+
export interface Deco { index: number; decoration: number; decorationName: string; surah: number; ayah: number; line: number; x0: number; y0: number; x1: number; y1: number; text: string; firstPath: number; nPaths: number }
|
|
91
|
+
/** The one hit shape for every hit test; the exact variants report distance 0 and `isExact`. */
|
|
92
|
+
export interface Hit { word: number; path: number; decoration: number; line: number; distance: number; isExact: boolean; wordKey: string | null; ayahKey: string | null }
|
|
93
|
+
export interface Ayah { index: number; surah: number; ayah: number; fragment: number; fragments: number; flags: number; rubuAlHizb: number; firstWord: number; nWords: number; ayahMarkDecoration: number; bbox: number[] }
|
|
94
|
+
export interface Line { index: number; lineNumber: number; isHeader: boolean; firstWord: number; nWords: number; bbox: number[]; bandY0: number; bandY1: number; centre: number }
|
|
105
95
|
export interface SelectionInfo { words: number[]; text: string; citation: string; textWithCitation: string }
|
|
106
|
-
export interface PageInfo { page: number; width: number; height: number; nLines: number; nAyahs: number; nWords: number; nPaths: number;
|
|
107
|
-
export interface Match { word: number; index: number;
|
|
108
|
-
export interface Surah { number: number; ayahCount: number; hasBanner: boolean; hasBasmalah: boolean; place: string;
|
|
109
|
-
export interface Division {
|
|
110
|
-
export interface Marker {
|
|
111
|
-
export interface
|
|
112
|
-
export interface
|
|
113
|
-
export interface
|
|
114
|
-
export interface
|
|
115
|
-
export interface
|
|
116
|
-
export interface
|
|
96
|
+
export interface PageInfo { page: number; width: number; height: number; nLines: number; nAyahs: number; nWords: number; nPaths: number; nDecorations: number; lineSpacing: number; forms: Form[]; loadMs: number; bytes: number; uri?: string }
|
|
97
|
+
export interface Match { word: number; index: number; isLooseMatch: boolean; wordKey: string; text: string }
|
|
98
|
+
export interface Surah { number: number; ayahCount: number; hasBanner: boolean; hasBasmalah: boolean; place: string; bannerDecoration: number; arabic: string; latin: string; english: string }
|
|
99
|
+
export interface Division { division: DivisionKind; number: number; surah: number; ayah: number; line: number; ayahIndex: number }
|
|
100
|
+
export interface Marker { decoration: number; surah: number; ayah: number; line: number; cx: number; cy: number; r: number; ornamentPath: number; numeralPath: number }
|
|
101
|
+
export interface SurahHeader { decoration: number; surah: number; line: number; x0: number; y0: number; x1: number; y1: number; titleX0: number; titleY0: number; titleX1: number; titleY1: number }
|
|
102
|
+
export interface Rosette { decoration: number; surah: number; ayah: number; juz: number; hizb: number; nisf: number; rubuAlHizb: number; rubuAlHizbInHizb: number }
|
|
103
|
+
export interface Sajdah { decoration: number; surah: number; ayah: number; signPath: number }
|
|
104
|
+
export interface CropBox { x0: number; y0: number; x1: number; y1: number; nWords: number; ayahMarkDecoration: number }
|
|
105
|
+
export interface Layout { scale: number; offsetX: number; offsetY: number; contentW: number; contentH: number; lineSpacing: number; lineDy: number[]; slots: number[][] }
|
|
106
|
+
export interface Stats { loadMs: number; bytes: number; baseMs: number; overlayMs: number; basePaths: number; overlayPaths: number; bands: number; hitUs: number; animating: boolean; styleHandles: number; highlightHandles: number; engineVersion: string; viewScale: number; layout: Layout | null }
|
|
107
|
+
export interface AtlasSurah { number: number; page: number; ayahCount: number; place: string; arabic: string; latin: string; english: string }
|
|
117
108
|
export interface AtlasRubuAlHizb { rubuAlHizb: number; surah: number; ayah: number; page: number; ayahKey: string }
|
|
118
109
|
|
|
119
|
-
export interface HighlightStyle { mode?: HighlightMode; ink?: Color; band?: Color; height?: '
|
|
120
|
-
/** One declarative highlight; the native side keeps id → handle and calls highlight /
|
|
110
|
+
export interface HighlightStyle { mode?: HighlightMode; ink?: Color; band?: Color; height?: 'lineSpacing' | 'ink'; padX?: number; padY?: number; radius?: number; seam?: number; ms?: number; layer?: number }
|
|
111
|
+
/** One declarative highlight; the native side keeps id → handle and calls highlight / moveHighlight / restyleHighlight / removeHighlight on diff. */
|
|
121
112
|
export interface Highlight { id: string; target: Target; style?: HighlightStyle }
|
|
122
113
|
/** One declarative style rule: `selector` (→ page.style) or `target` (→ page.styleTarget); `hide` → page.hide(selector). */
|
|
123
114
|
export interface StyleRule { id: string; selector?: Selector; target?: Target; color?: Color; ms?: number; layer?: number; hide?: boolean }
|
|
124
115
|
export interface Theme { ink?: Color; diacritics?: Color; dots?: Color; waqf?: Color; sifr?: Color; ayahMark?: Color; numeral?: Color; headers?: Color; marks?: Record<string, Color>; ms?: number }
|
|
125
|
-
|
|
116
|
+
/** `from` masks every word from that index to the end of the page instead of `target`. */
|
|
117
|
+
export interface Mask { target?: Target; from?: number; mode?: MaskMode; blockColor?: Color; padX?: number; padY?: number; radius?: number; reverse?: boolean }
|
|
126
118
|
/** Greyed-page reveal: `revealStart(lit, byAyah, grey, ink, ayahMarks, ms)` then `revealGoto(at)` whenever `at` changes; null → revealStop. */
|
|
127
119
|
export interface Reveal { lit?: number; byAyah?: boolean; grey?: Color; ink?: Color; ayahMarks?: boolean; ms?: number; at?: number }
|
|
128
120
|
|
|
129
121
|
// ── the view ─────────────────────────────────────────────────────────────────────────────────
|
|
122
|
+
/** What a pinch does to the page: the engine owns the policy, this names which one. */
|
|
123
|
+
export type ZoomMode = 'stepped' | 'continuous' | 'magnify';
|
|
124
|
+
/** Where the reader's zoom control stands: the mode, the step, and the size in force. */
|
|
125
|
+
export interface Zoom { mode: ZoomMode; step: number; zoom: number }
|
|
126
|
+
|
|
130
127
|
export interface QvpPageViewProps extends ViewProps {
|
|
131
128
|
/** 'asset://pages/042.qvp' | 'file:///…' | '/abs/path' | 'base64:…' — the package ships no page data. */
|
|
132
129
|
pageUri?: string;
|
|
@@ -134,29 +131,47 @@ export interface QvpPageViewProps extends ViewProps {
|
|
|
134
131
|
/** optional NNN.words.json sidecar (derived forms), same URI schemes */
|
|
135
132
|
wordsUri?: string;
|
|
136
133
|
padTop?: number; padBottom?: number; padSide?: number;
|
|
137
|
-
lineSpacing
|
|
134
|
+
/** spacing only opens up: lineSpacing < 1 is clamped by the engine */
|
|
135
|
+
lineSpacing?: number; fillHeight?: boolean;
|
|
136
|
+
/** Draw source-native frames around surah names on the printed page. */
|
|
137
|
+
surahFrames?: boolean;
|
|
138
138
|
paperColor?: Color; defaultInk?: Color; selectionBand?: Color;
|
|
139
139
|
selectionEnabled?: boolean; zoomEnabled?: boolean; hitMaxDistance?: number;
|
|
140
|
+
/** What a pinch does to the page. `stepped` reflows onto the page's own zoom steps and is
|
|
141
|
+
* the default a reader gets; `continuous` reflows to the zoom the fingers ask for; `magnify`
|
|
142
|
+
* scales the printed page and leaves the rows alone. */
|
|
143
|
+
zoomMode?: ZoomMode;
|
|
144
|
+
/** The step to sit on: 0 is the printed page, 1 upwards the page's own steps. Leave it out
|
|
145
|
+
* to let the reader's pinch drive it. */
|
|
146
|
+
zoomStep?: number;
|
|
140
147
|
theme?: Theme | null;
|
|
141
148
|
styles?: StyleRule[];
|
|
142
149
|
highlights?: Highlight[];
|
|
143
150
|
mask?: Mask | null;
|
|
144
151
|
reveal?: Reveal | null;
|
|
145
152
|
onWordTap?: (e: { word: Word; hit: Hit }) => void;
|
|
146
|
-
|
|
153
|
+
onDecorationTap?: (e: { decoration: Deco; hit: Hit }) => void;
|
|
147
154
|
onEmptyTap?: () => void;
|
|
148
155
|
onSelectionChanged?: (e: SelectionInfo) => void;
|
|
149
156
|
onPageLoad?: (e: PageInfo) => void;
|
|
150
157
|
onRevealChanged?: (e: { steps: number; at: number | null }) => void;
|
|
158
|
+
/** A sideways flick the engine read as a page turn, in pages, in reading order: a mushaf is
|
|
159
|
+
* read right to left, so a flick to the right gives +1. Which page that is belongs to the
|
|
160
|
+
* app, so the view turns nothing by itself. */
|
|
161
|
+
onSwipe?: (e: { pages: number }) => void;
|
|
162
|
+
/** The size the reader is at changed: a pinch committed, or a step was asked for. */
|
|
163
|
+
onZoomChanged?: (e: Zoom) => void;
|
|
151
164
|
onError?: (e: { message: string }) => void;
|
|
152
165
|
}
|
|
153
|
-
type NativeProps = ViewProps & Omit<QvpPageViewProps, 'onWordTap' | '
|
|
166
|
+
type NativeProps = ViewProps & Omit<QvpPageViewProps, 'onWordTap' | 'onDecorationTap' | 'onEmptyTap' | 'onSelectionChanged' | 'onPageLoad' | 'onRevealChanged' | 'onSwipe' | 'onZoomChanged' | 'onError'> & {
|
|
154
167
|
onWordTap?: (e: NativeSyntheticEvent<{ word: Word; hit: Hit }>) => void;
|
|
155
|
-
|
|
168
|
+
onDecorationTap?: (e: NativeSyntheticEvent<{ decoration: Deco; hit: Hit }>) => void;
|
|
156
169
|
onEmptyTap?: (e: NativeSyntheticEvent<{}>) => void;
|
|
157
170
|
onSelectionChanged?: (e: NativeSyntheticEvent<SelectionInfo>) => void;
|
|
158
171
|
onPageLoad?: (e: NativeSyntheticEvent<PageInfo>) => void;
|
|
159
172
|
onRevealChanged?: (e: NativeSyntheticEvent<{ steps: number; at: number | null }>) => void;
|
|
173
|
+
onSwipe?: (e: NativeSyntheticEvent<{ pages: number }>) => void;
|
|
174
|
+
onZoomChanged?: (e: NativeSyntheticEvent<Zoom>) => void;
|
|
160
175
|
onError?: (e: NativeSyntheticEvent<{ message: string }>) => void;
|
|
161
176
|
};
|
|
162
177
|
const NativeQvpPageView = requireNativeComponent<NativeProps>('QvpPageView') as HostComponent<NativeProps>;
|
|
@@ -180,7 +195,7 @@ export type QvpPageViewHandle = PageApi & { tag: () => number };
|
|
|
180
195
|
|
|
181
196
|
export const QvpPageView = forwardRef<QvpPageViewHandle, QvpPageViewProps>(function QvpPageView(props, ref) {
|
|
182
197
|
const native = useRef<any>(null);
|
|
183
|
-
const { theme, styles, highlights, mask, reveal, paperColor, defaultInk, selectionBand, onWordTap,
|
|
198
|
+
const { theme, styles, highlights, mask, reveal, paperColor, defaultInk, selectionBand, onWordTap, onDecorationTap, onEmptyTap, onSelectionChanged, onPageLoad, onRevealChanged, onSwipe, onZoomChanged, onError, ...rest } = props;
|
|
184
199
|
const tag = () => findNodeHandle(native.current) ?? -1;
|
|
185
200
|
useImperativeHandle(ref, () => ({ tag, ...bindPage(tag) }), []);
|
|
186
201
|
const nTheme = useMemo(() => (theme ? normColors(theme) : null), [theme]);
|
|
@@ -200,54 +215,77 @@ export const QvpPageView = forwardRef<QvpPageViewHandle, QvpPageViewProps>(funct
|
|
|
200
215
|
highlights={nHighlights}
|
|
201
216
|
mask={nMask}
|
|
202
217
|
reveal={nReveal}
|
|
203
|
-
onWordTap={onWordTap ? e => onWordTap(e.nativeEvent) : undefined}
|
|
204
|
-
|
|
218
|
+
onWordTap={onWordTap ? (e: NativeSyntheticEvent<{ word: Word; hit: Hit }>) => onWordTap(e.nativeEvent) : undefined}
|
|
219
|
+
onDecorationTap={onDecorationTap ? (e: NativeSyntheticEvent<{ decoration: Deco; hit: Hit }>) => onDecorationTap(e.nativeEvent) : undefined}
|
|
205
220
|
onEmptyTap={onEmptyTap ? () => onEmptyTap() : undefined}
|
|
206
|
-
onSelectionChanged={onSelectionChanged ? e => onSelectionChanged(e.nativeEvent) : undefined}
|
|
207
|
-
onPageLoad={onPageLoad ? e => onPageLoad(e.nativeEvent) : undefined}
|
|
208
|
-
onRevealChanged={onRevealChanged ? e => onRevealChanged(e.nativeEvent) : undefined}
|
|
209
|
-
|
|
221
|
+
onSelectionChanged={onSelectionChanged ? (e: NativeSyntheticEvent<SelectionInfo>) => onSelectionChanged(e.nativeEvent) : undefined}
|
|
222
|
+
onPageLoad={onPageLoad ? (e: NativeSyntheticEvent<PageInfo>) => onPageLoad(e.nativeEvent) : undefined}
|
|
223
|
+
onRevealChanged={onRevealChanged ? (e: NativeSyntheticEvent<{ steps: number; at: number | null }>) => onRevealChanged(e.nativeEvent) : undefined}
|
|
224
|
+
onSwipe={onSwipe ? (e: NativeSyntheticEvent<{ pages: number }>) => onSwipe(e.nativeEvent) : undefined}
|
|
225
|
+
onZoomChanged={onZoomChanged ? (e: NativeSyntheticEvent<Zoom>) => onZoomChanged(e.nativeEvent) : undefined}
|
|
226
|
+
onError={onError ? (e: NativeSyntheticEvent<{ message: string }>) => onError(e.nativeEvent) : undefined}
|
|
210
227
|
/>
|
|
211
228
|
);
|
|
212
229
|
});
|
|
213
230
|
|
|
214
231
|
// ── the module ───────────────────────────────────────────────────────────────────────────────
|
|
215
232
|
const M = NativeModules.QvpModule as any;
|
|
233
|
+
|
|
234
|
+
// The engine's name tables, read through the native module's constants; no table lives here.
|
|
235
|
+
const NAMES = (M.getConstants?.() ?? {}) as { marks?: string[]; kinds?: string[]; families?: string[]; categories?: string[]; decorations?: string[]; divisions?: string[]; places?: string[] };
|
|
236
|
+
const KIND_NAMES: string[] = NAMES.kinds ?? [];
|
|
237
|
+
const FAMILY_NAMES: string[] = NAMES.families ?? [];
|
|
238
|
+
const CATEGORY_NAMES: string[] = NAMES.categories ?? [];
|
|
239
|
+
const DECORATION_NAMES: string[] = NAMES.decorations ?? [];
|
|
240
|
+
export const MARKS: string[] = NAMES.marks ?? [];
|
|
241
|
+
/** A name's id in a table; 255 when the table has no such name. */
|
|
242
|
+
export type NameTable = 'marks' | 'kinds' | 'families' | 'categories' | 'decorations' | 'divisions' | 'places';
|
|
243
|
+
const named = (v: string | number, names: string[]) => { if (typeof v === 'number') return v; const i = names.indexOf(v); return i < 0 ? 255 : i; };
|
|
244
|
+
export function markId(name: string | number): number { return named(name, MARKS); }
|
|
216
245
|
if (!M) throw new Error('@quran.ws/qvp-react-native: native module QvpModule not linked (Android only for now; see README)');
|
|
217
246
|
|
|
218
|
-
export interface SearchOptions { form?: Form; mode?: 'includes' | 'exact' | 'prefix'; normalize?: boolean;
|
|
247
|
+
export interface SearchOptions { form?: Form; mode?: 'includes' | 'exact' | 'prefix'; normalize?: boolean; looseMatch?: boolean; limit?: number }
|
|
219
248
|
export interface TextOptions { form?: Form; wordSep?: string; lineSep?: string }
|
|
220
249
|
export interface CropOptions { pad?: number; keepAyahMarks?: boolean; background?: Color }
|
|
221
|
-
export interface HitOptions { maxDistance?: number; gapBias?: number;
|
|
250
|
+
export interface HitOptions { maxDistance?: number; gapBias?: number; preferExact?: boolean }
|
|
222
251
|
|
|
223
252
|
/** Page-bound queries; `tag` is the react tag of a mounted `<QvpPageView />` (`findNodeHandle`). */
|
|
224
253
|
export const Qvp = {
|
|
225
254
|
info: (tag: number): Promise<PageInfo> => M.info(tag),
|
|
226
255
|
words: (tag: number): Promise<Word[]> => M.words(tag),
|
|
227
|
-
word: (tag: number,
|
|
256
|
+
word: (tag: number, index: number): Promise<Word | null> => M.word(tag, index),
|
|
228
257
|
ayahs: (tag: number): Promise<Ayah[]> => M.ayahs(tag),
|
|
229
258
|
lines: (tag: number): Promise<Line[]> => M.lines(tag),
|
|
230
|
-
|
|
259
|
+
decorations: (tag: number): Promise<Deco[]> => M.decorations(tag),
|
|
231
260
|
findWord: (tag: number, s: number, a: number, w: number): Promise<number> => M.findWord(tag, s, a, w),
|
|
232
|
-
|
|
233
|
-
wordForm: (tag: number,
|
|
261
|
+
targetWords: (tag: number, target: Target): Promise<number[]> => M.targetWords(tag, target),
|
|
262
|
+
wordForm: (tag: number, index: number, form: Form = 'rasmUthmani'): Promise<string> => M.wordForm(tag, index, form),
|
|
234
263
|
hasForm: (tag: number, form: Form): Promise<boolean> => M.hasForm(tag, form),
|
|
235
264
|
attachWords: (tag: number, json: string | object): Promise<number> => M.attachWords(tag, typeof json === 'string' ? json : JSON.stringify(json)),
|
|
236
265
|
surahs: (tag: number): Promise<Surah[]> => M.surahs(tag),
|
|
237
266
|
divisions: (tag: number): Promise<Division[]> => M.divisions(tag),
|
|
267
|
+
surahHeaders: (tag: number): Promise<SurahHeader[]> => M.surahHeaders(tag),
|
|
268
|
+
surahHeadersView: (tag: number): Promise<SurahHeader[]> => M.surahHeadersView(tag),
|
|
238
269
|
ayahMarks: (tag: number): Promise<Marker[]> => M.ayahMarks(tag),
|
|
239
270
|
rosettes: (tag: number): Promise<Rosette[]> => M.rosettes(tag),
|
|
240
271
|
sajdahs: (tag: number): Promise<Sajdah[]> => M.sajdahs(tag),
|
|
241
272
|
ayahKeys: (tag: number): Promise<{ surah: number; ayah: number; ayahKey: string }[]> => M.ayahKeys(tag),
|
|
242
|
-
ayahWordCount: (tag: number, s: number, a: number): Promise<{ count: number;
|
|
273
|
+
ayahWordCount: (tag: number, s: number, a: number): Promise<{ count: number; isComplete: boolean }> => M.ayahWordCount(tag, s, a),
|
|
243
274
|
reciteMap: (tag: number, s: number, a: number, nSegments: number): Promise<number[] | null> => M.reciteMap(tag, s, a, nSegments),
|
|
244
275
|
wordLabel: (tag: number, i: number): Promise<string> => M.wordLabel(tag, i),
|
|
245
276
|
ayahLabel: (tag: number, i: number): Promise<string> => M.ayahLabel(tag, i),
|
|
246
277
|
text: (tag: number, target: Target = 'page', opts?: TextOptions): Promise<string> => M.text(tag, target, opts ?? null),
|
|
247
278
|
search: (tag: number, query: string, opts?: SearchOptions): Promise<Match[]> => M.search(tag, query, opts ?? null),
|
|
248
279
|
citation: (tag: number, words: number[]): Promise<string> => M.citation(tag, words),
|
|
249
|
-
|
|
250
|
-
|
|
280
|
+
hitTestExact: (tag: number, x: number, y: number): Promise<Hit | null> => M.hitTestExact(tag, x, y),
|
|
281
|
+
hitTest: (tag: number, x: number, y: number, opts?: HitOptions): Promise<Hit | null> => M.hitTest(tag, x, y, opts ?? null),
|
|
282
|
+
hitTestExactView: (tag: number, x: number, y: number): Promise<Hit | null> => M.hitTestExactView(tag, x, y),
|
|
283
|
+
hitTestView: (tag: number, x: number, y: number, opts?: HitOptions): Promise<Hit | null> => M.hitTestView(tag, x, y, opts ?? null),
|
|
284
|
+
layoutLineSpacingToFill: (tag: number, max = 0): Promise<number> => M.layoutLineSpacingToFill(tag, max),
|
|
285
|
+
layoutWastedFraction: (tag: number): Promise<number> => M.layoutWastedFraction(tag),
|
|
286
|
+
layoutPrintedHeight: (tag: number): Promise<number> => M.layoutPrintedHeight(tag),
|
|
287
|
+
grid: (tag: number): Promise<{ lines: number; lineSpacing: number }> => M.grid(tag),
|
|
288
|
+
wordBoundsView: (tag: number, i: number): Promise<{ x0: number; y0: number; x1: number; y1: number } | null> => M.wordBoundsView(tag, i),
|
|
251
289
|
currentLayout: (tag: number): Promise<Layout | null> => M.currentLayout(tag),
|
|
252
290
|
relayout: (tag: number): Promise<void> => M.relayout(tag),
|
|
253
291
|
resetView: (tag: number): Promise<void> => M.resetView(tag),
|
|
@@ -255,19 +293,19 @@ export const Qvp = {
|
|
|
255
293
|
select: (tag: number, anchor: number, focus: number = anchor): Promise<SelectionInfo> => M.select(tag, anchor, focus),
|
|
256
294
|
clearSelection: (tag: number): Promise<void> => M.clearSelection(tag),
|
|
257
295
|
selection: (tag: number): Promise<SelectionInfo> => M.selection(tag),
|
|
258
|
-
selectionText: (tag: number, form: Form = 'rasmUthmani',
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
296
|
+
selectionText: (tag: number, form: Form = 'rasmUthmani', includeCitation = false): Promise<string> => M.selectionText(tag, form, includeCitation),
|
|
297
|
+
unmaskNext: (tag: number, n = 1): Promise<number> => M.unmaskNext(tag, n),
|
|
298
|
+
maskBack: (tag: number, n = 1): Promise<number> => M.maskBack(tag, n),
|
|
299
|
+
unmaskWord: (tag: number, i: number): Promise<boolean> => M.unmaskWord(tag, i),
|
|
300
|
+
maskWord: (tag: number, i: number): Promise<boolean> => M.maskWord(tag, i),
|
|
301
|
+
unmaskAll: (tag: number): Promise<void> => M.unmaskAll(tag),
|
|
302
|
+
maskAll: (tag: number): Promise<void> => M.maskAll(tag),
|
|
265
303
|
maskHidden: (tag: number): Promise<number[]> => M.maskHidden(tag),
|
|
266
304
|
maskWords: (tag: number): Promise<number[]> => M.maskWords(tag),
|
|
267
|
-
|
|
268
|
-
|
|
305
|
+
revealStepCount: (tag: number): Promise<number> => M.revealStepCount(tag),
|
|
306
|
+
revealPosition: (tag: number): Promise<number | null> => M.revealPosition(tag),
|
|
269
307
|
revealStepOf: (tag: number, i: number): Promise<number> => M.revealStepOf(tag, i),
|
|
270
|
-
|
|
308
|
+
cropBounds: (tag: number, target: Target, opts?: CropOptions): Promise<CropBox | null> => M.cropBounds(tag, target, opts ?? null),
|
|
271
309
|
cropSvg: (tag: number, target: Target, opts?: CropOptions): Promise<string | null> => M.cropSvg(tag, target, opts ? { ...opts, background: css(opts.background) } : null),
|
|
272
310
|
|
|
273
311
|
// engine-wide
|
|
@@ -275,29 +313,34 @@ export const Qvp = {
|
|
|
275
313
|
fold: (s: string): Promise<string> => M.arabic('fold', s),
|
|
276
314
|
normalize: (s: string): Promise<string> => M.arabic('normalize', s),
|
|
277
315
|
looseKey: (s: string): Promise<string> => M.arabic('loose', s),
|
|
278
|
-
arabic: (
|
|
279
|
-
|
|
280
|
-
|
|
316
|
+
arabic: (op: 'strip' | 'fold' | 'normalize' | 'loose', s: string): Promise<string> => M.arabic(op, s),
|
|
317
|
+
markCategory: (m: number): Promise<number> => M.markCategory(m),
|
|
318
|
+
engineName: (): Promise<string> => M.engineName(),
|
|
319
|
+
/** How many ids a name table has, and a name's id in it (255 when absent); the tables come from the engine at start. */
|
|
320
|
+
nameCount: (table: NameTable): number => (NAMES[table] ?? []).length,
|
|
321
|
+
nameId: (table: NameTable, name: string): number => named(name, NAMES[table] ?? []),
|
|
281
322
|
markName: (m: number): string => MARKS[m] ?? 'unknown',
|
|
282
323
|
kindName: (k: number): string => KIND_NAMES[k] ?? 'other',
|
|
283
324
|
categoryName: (c: number): string => CATEGORY_NAMES[c] ?? '',
|
|
284
325
|
familyName: (f: number): string => FAMILY_NAMES[f] ?? '',
|
|
285
|
-
version
|
|
326
|
+
/** The engine version, e.g. '0.2.0', and the page format version it reads. */
|
|
327
|
+
version: (M.getConstants?.().version ?? '') as string,
|
|
328
|
+
formatVersion: (M.getConstants?.().formatVersion ?? 0) as number,
|
|
286
329
|
|
|
287
330
|
// atlas
|
|
288
331
|
loadAtlas: (uri: string): Promise<number> => M.loadAtlas(uri),
|
|
289
332
|
freeAtlas: (id: number): Promise<void> => M.freeAtlas(id),
|
|
290
333
|
atlasPageOf: (id: number, s: number, a: number): Promise<number | null> => M.atlasPageOf(id, s, a),
|
|
291
334
|
atlasPageRange: (id: number, page: number): Promise<{ first: { surah: number; ayah: number }; last: { surah: number; ayah: number } } | null> => M.atlasPageRange(id, page),
|
|
292
|
-
|
|
335
|
+
atlasPageCount: (id: number): Promise<number> => M.atlasPageCount(id),
|
|
293
336
|
atlasSurah: (id: number, n: number): Promise<AtlasSurah | null> => M.atlasSurah(id, n),
|
|
294
337
|
atlasSurahs: (id: number): Promise<AtlasSurah[]> => M.atlasSurahs(id),
|
|
295
338
|
atlasPageOfSurah: (id: number, n: number): Promise<number | null> => M.atlasPageOfSurah(id, n),
|
|
296
|
-
atlasDivision: (id: number,
|
|
297
|
-
|
|
298
|
-
|
|
339
|
+
atlasDivision: (id: number, division: DivisionKind, n: number): Promise<AtlasRubuAlHizb | null> => M.atlasDivision(id, division, n),
|
|
340
|
+
atlasDivisionOf: (id: number, division: DivisionKind, s: number, a: number): Promise<number | null> => M.atlasDivisionOf(id, division, s, a),
|
|
341
|
+
atlasJuzOf: (id: number, s: number, a: number): Promise<number | null> => M.atlasJuzOf(id, s, a),
|
|
299
342
|
atlasPagesOfJuz: (id: number, n: number): Promise<[number, number] | null> => M.atlasPagesOfJuz(id, n),
|
|
300
|
-
|
|
343
|
+
atlasSearchSurahs: (id: number, text: string): Promise<AtlasSurah[]> => M.atlasSearchSurahs(id, text),
|
|
301
344
|
};
|
|
302
345
|
|
|
303
346
|
/** Cross-page lookup (atlas.qva). `const atlas = await QvpAtlas.load('asset://pages/atlas.qva')`. */
|
|
@@ -307,22 +350,22 @@ export class QvpAtlas {
|
|
|
307
350
|
free() { return Qvp.freeAtlas(this.id); }
|
|
308
351
|
pageOf(s: number, a: number) { return Qvp.atlasPageOf(this.id, s, a); }
|
|
309
352
|
pageRange(page: number) { return Qvp.atlasPageRange(this.id, page); }
|
|
310
|
-
|
|
353
|
+
pageCount() { return Qvp.atlasPageCount(this.id); }
|
|
311
354
|
surah(n: number) { return Qvp.atlasSurah(this.id, n); }
|
|
312
355
|
surahs() { return Qvp.atlasSurahs(this.id); }
|
|
313
356
|
pageOfSurah(n: number) { return Qvp.atlasPageOfSurah(this.id, n); }
|
|
314
|
-
division(
|
|
357
|
+
division(division: DivisionKind, n: number) { return Qvp.atlasDivision(this.id, division, n); }
|
|
315
358
|
juz(n: number) { return Qvp.atlasDivision(this.id, 'juz', n); }
|
|
316
359
|
hizb(n: number) { return Qvp.atlasDivision(this.id, 'hizb', n); }
|
|
317
360
|
rubuAlHizb(n: number) { return Qvp.atlasDivision(this.id, 'rubuAlHizb', n); }
|
|
318
|
-
|
|
319
|
-
|
|
361
|
+
divisionOf(division: DivisionKind, s: number, a: number) { return Qvp.atlasDivisionOf(this.id, division, s, a); }
|
|
362
|
+
juzOf(s: number, a: number) { return Qvp.atlasJuzOf(this.id, s, a); }
|
|
320
363
|
pagesOfJuz(n: number) { return Qvp.atlasPagesOfJuz(this.id, n); }
|
|
321
|
-
|
|
364
|
+
searchSurahs(text: string) { return Qvp.atlasSearchSurahs(this.id, text); }
|
|
322
365
|
}
|
|
323
366
|
|
|
324
367
|
type Tail<F> = F extends (tag: number, ...rest: infer R) => infer Ret ? (...rest: R) => Ret : never;
|
|
325
|
-
const pageMethods = ['info', 'words', 'word', 'ayahs', 'lines', '
|
|
368
|
+
const pageMethods = ['info', 'words', 'word', 'ayahs', 'lines', 'decorations', 'findWord', 'targetWords', 'wordForm', 'hasForm', 'attachWords', 'surahs', 'divisions', 'surahHeaders', 'surahHeadersView', 'ayahMarks', 'rosettes', 'sajdahs', 'ayahKeys', 'ayahWordCount', 'reciteMap', 'wordLabel', 'ayahLabel', 'text', 'search', 'citation', 'hitTestExact', 'hitTest', 'hitTestExactView', 'hitTestView', 'layoutLineSpacingToFill', 'layoutWastedFraction', 'layoutPrintedHeight', 'grid', 'wordBoundsView', 'currentLayout', 'relayout', 'resetView', 'stats', 'select', 'clearSelection', 'selection', 'selectionText', 'unmaskNext', 'maskBack', 'unmaskWord', 'maskWord', 'unmaskAll', 'maskAll', 'maskHidden', 'maskWords', 'revealStepCount', 'revealPosition', 'revealStepOf', 'cropBounds', 'cropSvg'] as const;
|
|
326
369
|
type PageMethod = (typeof pageMethods)[number];
|
|
327
370
|
export type PageApi = { [K in PageMethod]: Tail<(typeof Qvp)[K]> };
|
|
328
371
|
function bindPage(tag: () => number): PageApi {
|