@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/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 (`net.quranpedia.qvp`), which is JNI over crates/qvp-ffi/include/qvp.h.
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 DECO = { AYAH_MARK: 0, SURAH_NAME: 1, BASMALAH: 2, DIVISION_MARK: 3, SAJDAH_MARK: 4 } as 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 { kind: number; a?: number; b?: number; c?: number; words?: number[] }
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 => ({ kind: 0 }),
61
- word: (i: number): TargetObj => ({ kind: 1, a: i }),
62
- words: (ws: number[]): TargetObj => ({ kind: 2, words: ws }),
63
- ayah: (s: number, a: number): TargetObj => ({ kind: 3, a: s, b: a }),
64
- ayahRange: (s: number, a: number, b: number): TargetObj => ({ kind: 4, a: s, b: a, c: b }),
65
- line: (n: number): TargetObj => ({ kind: 5, a: n }),
66
- surah: (s: number): TargetObj => ({ kind: 6, a: s }),
67
- range: (a: number, b: number): TargetObj => ({ kind: 7, a, b }),
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 { kind: number; a?: number; b?: number; c?: number; mark?: string }
58
+ export interface Selector { selector: number; a?: number; b?: number; c?: number; mark?: string }
70
59
  export const Sel = {
71
- page: (): Selector => ({ kind: 0 }),
72
- path: (p: number): Selector => ({ kind: 1, a: p }),
73
- wordPath: (w: number, nth: number): Selector => ({ kind: 2, a: w, b: nth }),
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 => ({ kind: 3, a: w, b: nth }),
76
- wordMarkNamed: (w: number, mark: string, nth = 0): Selector => ({ kind: 4, a: w, b: markId(mark), c: nth, mark }),
77
- wordBody: (w: number): Selector => ({ kind: 5, a: w }),
78
- wordMarks: (w: number): Selector => ({ kind: 6, a: w }),
79
- word: (w: number): Selector => ({ kind: 7, a: w }),
80
- ayah: (s: number, a: number): Selector => ({ kind: 8, a: s, b: a }),
81
- line: (n: number): Selector => ({ kind: 9, a: n }),
82
- mark: (m: string | number): Selector => ({ kind: 10, a: markId(m) }),
83
- category: (c: string | number): Selector => ({ kind: 11, a: named(c, CATEGORY_NAMES) }),
84
- family: (f: string | number): Selector => ({ kind: 12, a: named(f, FAMILY_NAMES) }),
85
- kind: (k: string | number): Selector => ({ kind: 13, a: named(k, KIND_NAMES) }),
86
- deco: (k: string | number): Selector => ({ kind: 14, a: named(k, DECO_NAMES) }),
87
- decoIdx: (d: number): Selector => ({ kind: 15, a: d }),
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 { idx: number; kind: number; kindName: string; mark: number; markName: string; nthMark: number; nthInWord: number; category: number; categoryName: string; family: number; familyName: string; line: number }
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
- idx: number; surah: number; ayah: number; word: number; line: number; lineIdx: number; ayahIdx: number;
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 { idx: number; kind: number; kindName: string; surah: number; ayah: number; line: number; x0: number; y0: number; x1: number; y1: number; text: string; firstPath: number; nPaths: number }
102
- export interface Hit { word: number; path: number; deco: number; line: number; distance: number; exact: boolean; wordKey: string | null; ayahKey: string | null }
103
- export interface Ayah { idx: number; surah: number; ayah: number; fragment: number; fragments: number; flags: number; rubuAlHizb: number; firstWord: number; nWords: number; ayahMarkDeco: number; bbox: number[] }
104
- export interface Line { idx: number; lineNo: number; isHeader: boolean; firstWord: number; nWords: number; bbox: number[]; bandY0: number; bandY1: number; centre: number }
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; nDecos: number; naturalPitch: number; forms: Form[]; loadMs: number; bytes: number; uri?: string }
107
- export interface Match { word: number; index: number; loose: boolean; wordKey: string; text: string }
108
- export interface Surah { number: number; ayahCount: number; hasBanner: boolean; hasBasmalah: boolean; place: string; bannerDeco: number; arabic: string; latin: string; english: string }
109
- export interface Division { kind: DivisionKind; n: number; surah: number; ayah: number; line: number; ayahIdx: number }
110
- export interface Marker { deco: number; surah: number; ayah: number; line: number; cx: number; cy: number; r: number; ornamentPath: number; numeralPath: number }
111
- export interface Rosette { deco: number; surah: number; ayah: number; juz: number; hizb: number; nisf: number; rubuAlHizb: number; rubuAlHizbInHizb: number }
112
- export interface Sajdah { deco: number; surah: number; ayah: number; signPath: number }
113
- export interface CropBox { x0: number; y0: number; x1: number; y1: number; nWords: number; ayahMarkDeco: number }
114
- export interface Layout { scale: number; ox: number; oy: number; contentW: number; contentH: number; pitch: number; lineDy: number[]; slots: number[][] }
115
- 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: number; viewScale: number; layout: Layout | null }
116
- export interface AtlasSurah { n: number; number: number; page: number; ayahCount: number; place: string; arabic: string; latin: string; english: string }
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?: 'pitch' | 'ink'; padX?: number; padY?: number; radius?: number; seam?: number; ms?: number; layer?: number }
120
- /** One declarative highlight; the native side keeps id → handle and calls highlight / rehighlight / restyleHighlight / unhighlight on diff. */
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
- export interface Mask { target: Target; mode?: MaskMode; blockColor?: Color; padX?: number; padY?: number; radius?: number; reverse?: boolean }
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?: number; lineGap?: number; fillHeight?: boolean;
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
- onDecoTap?: (e: { deco: Deco; hit: Hit }) => void;
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' | 'onDecoTap' | 'onEmptyTap' | 'onSelectionChanged' | 'onPageLoad' | 'onRevealChanged' | 'onError'> & {
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
- onDecoTap?: (e: NativeSyntheticEvent<{ deco: Deco; hit: Hit }>) => void;
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, onDecoTap, onEmptyTap, onSelectionChanged, onPageLoad, onRevealChanged, onError, ...rest } = props;
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
- onDecoTap={onDecoTap ? e => onDecoTap(e.nativeEvent) : undefined}
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
- onError={onError ? e => onError(e.nativeEvent) : undefined}
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; loose?: boolean; limit?: number }
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; exactFirst?: boolean }
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, idx: number): Promise<Word | null> => M.word(tag, idx),
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
- decos: (tag: number): Promise<Deco[]> => M.decos(tag),
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
- resolve: (tag: number, target: Target): Promise<number[]> => M.resolve(tag, target),
233
- wordForm: (tag: number, idx: number, form: Form = 'rasmUthmani'): Promise<string> => M.wordForm(tag, idx, form),
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; complete: boolean }> => M.ayahWordCount(tag, s, a),
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
- hitTestViewEx: (tag: number, x: number, y: number, opts?: HitOptions): Promise<Hit | null> => M.hitTestViewEx(tag, x, y, opts ?? null),
250
- wordBoxView: (tag: number, i: number): Promise<{ x0: number; y0: number; x1: number; y1: number } | null> => M.wordBoxView(tag, i),
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', citation = false): Promise<string> => M.selectionText(tag, form, citation),
259
- revealNext: (tag: number, n = 1): Promise<number> => M.revealNext(tag, n),
260
- hideBack: (tag: number, n = 1): Promise<number> => M.hideBack(tag, n),
261
- revealWord: (tag: number, i: number): Promise<boolean> => M.revealWord(tag, i),
262
- hideWord: (tag: number, i: number): Promise<boolean> => M.hideWord(tag, i),
263
- revealAll: (tag: number): Promise<void> => M.revealAll(tag),
264
- hideAll: (tag: number): Promise<void> => M.hideAll(tag),
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
- revealSteps: (tag: number): Promise<number> => M.revealSteps(tag),
268
- revealAt: (tag: number): Promise<number | null> => M.revealAt(tag),
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
- cropBox: (tag: number, target: Target, opts?: CropOptions): Promise<CropBox | null> => M.cropBox(tag, target, opts ?? null),
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: (kind: 'strip' | 'fold' | 'normalize' | 'loose', s: string): Promise<string> => M.arabic(kind, s),
279
- gapToFill: (pageW: number, pageH: number, lines: number, viewW: number, viewH: number, max = 0): Promise<number> => M.gapToFill(pageW, pageH, lines, viewW, viewH, max),
280
- wastedFraction: (pageW: number, pageH: number, viewW: number, viewH: number): Promise<number> => M.wastedFraction(pageW, pageH, viewW, viewH),
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: (M.getConstants?.().version ?? 0) as number,
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
- atlasPages: (id: number): Promise<number> => M.atlasPages(id),
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, kind: DivisionKind, n: number): Promise<AtlasRubuAlHizb | null> => M.atlasDivision(id, kind, n),
297
- atlasDivisionAt: (id: number, kind: DivisionKind, s: number, a: number): Promise<number | null> => M.atlasDivisionAt(id, kind, s, a),
298
- atlasJuzAt: (id: number, s: number, a: number): Promise<number | null> => M.atlasJuzAt(id, s, a),
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
- atlasFindSurah: (id: number, text: string): Promise<AtlasSurah[]> => M.atlasFindSurah(id, text),
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
- pages() { return Qvp.atlasPages(this.id); }
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(kind: DivisionKind, n: number) { return Qvp.atlasDivision(this.id, kind, n); }
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
- divisionAt(kind: DivisionKind, s: number, a: number) { return Qvp.atlasDivisionAt(this.id, kind, s, a); }
319
- juzAt(s: number, a: number) { return Qvp.atlasJuzAt(this.id, s, a); }
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
- findSurah(text: string) { return Qvp.atlasFindSurah(this.id, text); }
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', 'decos', 'findWord', 'resolve', 'wordForm', 'hasForm', 'attachWords', 'surahs', 'divisions', 'ayahMarks', 'rosettes', 'sajdahs', 'ayahKeys', 'ayahWordCount', 'reciteMap', 'wordLabel', 'ayahLabel', 'text', 'search', 'citation', 'hitTestViewEx', 'wordBoxView', 'currentLayout', 'relayout', 'resetView', 'stats', 'select', 'clearSelection', 'selection', 'selectionText', 'revealNext', 'hideBack', 'revealWord', 'hideWord', 'revealAll', 'hideAll', 'maskHidden', 'maskWords', 'revealSteps', 'revealAt', 'revealStepOf', 'cropBox', 'cropSvg'] as const;
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 {