@quran.ws/qvp-react-native 0.1.0 → 0.3.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/LICENSE ADDED
@@ -0,0 +1,172 @@
1
+ Copyright 2026 Quran.ws
2
+
3
+ This work is dedicated as a waqf for the sake of Allah, seeking His pleasure
4
+ through the benefit it brings to people. We publish it to enable the widest
5
+ possible beneficial use, with as few restrictions as possible.
6
+
7
+ Quran.ws publishes code under the MIT License, and data and content under the
8
+ Creative Commons Attribution 4.0 International License,
9
+ with a standing waiver of attribution for use inside a product.
10
+
11
+ We license only what we have the right to license; third-party material keeps
12
+ the licence and terms of its own rights holders.
13
+
14
+ Full policy: https://github.com/quran-ws/guidelines
15
+
16
+
17
+ ═══════════════════════════════════════════════════════════════════════════════
18
+ WHAT IS COVERED BY WHAT
19
+ ═══════════════════════════════════════════════════════════════════════════════
20
+
21
+ crates/ MIT
22
+ docs/ CC BY 4.0
23
+ packages/ MIT
24
+ scripts/ MIT
25
+ web/ MIT
26
+
27
+ Where a directory or file carries its own LICENSE or NOTICE, that notice
28
+ governs for that material. Names, logos and trademarks are not licensed.
29
+
30
+
31
+ ═══════════════════════════════════════════════════════════════════════════════
32
+ ATTRIBUTION WAIVED FOR USE IN PRODUCTS
33
+ ═══════════════════════════════════════════════════════════════════════════════
34
+
35
+ As rights holders of our original data and content, we grant a permanent,
36
+ worldwide, royalty-free, irrevocable waiver of the attribution requirement of
37
+ CC BY 4.0 (Section 3(a)) when the material is used within an application,
38
+ website, service, API, bot, tool, research work, or product, whether free or
39
+ commercial. This permission expands what you may do and adds no restriction to
40
+ CC BY 4.0.
41
+
42
+ Attribution is still required on republication — when the data or content
43
+ itself is made available to others as a resource in its own right. The test:
44
+ if a third party can obtain the data AS DATA from what you distribute, that is
45
+ republication, and CC BY 4.0 attribution applies.
46
+
47
+ Suggested attribution:
48
+
49
+ Data from [resource] by Quran.ws, licensed under CC BY 4.0.
50
+ Source: [canonical URL]. Version: [version]. Modified: [yes/no].
51
+
52
+ Use for training, fine-tuning, evaluating, indexing or retrieval by machine
53
+ learning systems is permitted and treated as use, not republication.
54
+
55
+
56
+ ═══════════════════════════════════════════════════════════════════════════════
57
+ THIRD-PARTY MATERIAL — KING FAHD GLORIOUS QUR'AN PRINTING COMPLEX
58
+ ═══════════════════════════════════════════════════════════════════════════════
59
+
60
+ The Qur'anic text editions, mushaf typesettings and fonts in this repository
61
+ derive from material published by the King Fahd Glorious Qur'an Printing
62
+ Complex. That material is not ours to license. It is used under the Complex's
63
+ own published usage rights, reproduced in full below in Arabic and English.
64
+
65
+ Our processing, correction, organisation or transformation of that material
66
+ creates no ownership in it and does not override these terms.
67
+
68
+
69
+ ───────────────────────────────────────────────────────────── حقوق الاستخدام ───
70
+
71
+ بناء على موافقة معالي وزير الشؤون الإسلامية والدعوة والإرشاد، المشرف العام على
72
+ مجمع الملك فهد لطباعة المصحف الشريف، يتشـرَّف المجمع بإتاحة نسخة رقمية كاملة من
73
+ مصحف المدينة النبوية المطبوع في المجمع، وذلك بالصيغ التالية:
74
+
75
+ رسم المتجهات Illustrator
76
+ وثيقة بي دي إف PDF
77
+ صور ذات جودة عالية Images
78
+ خط حاسوبي True Type Font
79
+
80
+ ويمكن استخدام النسخة المذكورة مجاناً في المجالات الشخصية والأعمال الفردية كافة،
81
+ وأعمال الجهات الحكومية والجهات والمؤسسات الخاصة والأهلية، وأعمال الطباعة
82
+ الورقية*، والنشـر المكتبي والإعلامي، ومواقع الإنترنت والبرامج الحاسوبية وغيرها،
83
+ وذلك داخل المملكة العربية السعودية وخارجها.
84
+
85
+ نسأل الله أن ينفع المسلمين بهذا العمل المبارك.
86
+
87
+ والحمد لله رب العالمين
88
+
89
+ * أعمال طباعة المصاحف داخل المملكة واستيرادها من الخارج لغرض البيع التجاري
90
+ تخضع للأمر السامي الكريم ذي الرقم 136/8 المؤرخ في 1/2/1406هـ وأكّد على ذلك
91
+ الأمر السامي الكريم رقم 9/ب/46356 بتاريخ 28/9/1424هـ. والذي وجه بمنع طباعة
92
+ المصاحف في الداخل في غير المجمع، وكذلك منع استيرادها من الخارج لغرض البيع
93
+ التجاري.
94
+
95
+
96
+ ──────────────────────────────────────────────────────────────── Copyright ────
97
+
98
+ Based on the approval of His Excellency the Minister of Islamic Affairs,
99
+ Endowments, Da'wah and Guidance, General Supervisor of the King Fahd Qur'an
100
+ Printing Complex.
101
+
102
+ The Qur'an Printing Complex is honored to present to the Muslim public a
103
+ complete free digital copy of Mus'haf al-Madinah published by the Complex, in
104
+ the following formats:
105
+
106
+ • Adobe Illustrator files
107
+ • PDF files
108
+ • High quality images
109
+ • True Type Font
110
+
111
+ Mus'haf al-Madinah in these previous formats can be used for free in all
112
+ personal, individual businesses, in works of governmental departments &
113
+ agencies, in the publications of both private and national institutions, also
114
+ suitable for Qur'an printing,* digital publishing, & for media use, can be used
115
+ also in websites, software, and other similar intermediates.
116
+
117
+ We ask Allah that He renders this blessed project beneficial to all Muslims.
118
+
119
+ * Printing of Qur'anic copies in the Kingdom, and their import from outside for
120
+ sale are subject to the Royal Decree No 136/8 dated 1/2/1406 AH which has been
121
+ reiterated by the Royal Decree No 9/B/46356 dated 28/9/1424 AH which prohibit
122
+ printing of Qur'anic copies in the Kingdom except in the Complex, likewise
123
+ their import from outside for sale.
124
+
125
+
126
+ ═══════════════════════════════════════════════════════════════════════════════
127
+ ACCURACY AND NO WARRANTY
128
+ ═══════════════════════════════════════════════════════════════════════════════
129
+
130
+ This material is provided AS IS, without warranty of any kind, as stated in
131
+ the MIT License and in CC BY 4.0 (Sections 5 and 6).
132
+
133
+ Qur'anic text, transliteration, translation, recitation and mushaf rendering
134
+ may contain errors, and releases are corrected over time. Anyone publishing
135
+ Qur'anic text to end users is responsible for verifying it against an
136
+ authorised printed mushaf and for tracking our corrections.
137
+
138
+ Corrections: corrections@quran.ws — treated as a priority.
139
+ Licensing questions: legal@quran.ws
140
+
141
+
142
+ ═══════════════════════════════════════════════════════════════════════════════
143
+ FULL LICENCE TEXTS
144
+ ═══════════════════════════════════════════════════════════════════════════════
145
+
146
+ The full CC BY 4.0 legal text is in LICENSES/CC-BY-4.0.txt and at
147
+ https://creativecommons.org/licenses/by/4.0/legalcode
148
+
149
+ MIT License
150
+
151
+ Copyright (c) 2026 Quran.ws
152
+
153
+ Permission is hereby granted, free of charge, to any person obtaining a copy
154
+ of this software and associated documentation files (the "Software"), to deal
155
+ in the Software without restriction, including without limitation the rights
156
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
157
+ copies of the Software, and to permit persons to whom the Software is
158
+ furnished to do so, subject to the following conditions:
159
+
160
+ The above copyright notice and this permission notice shall be included in all
161
+ copies or substantial portions of the Software.
162
+
163
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
164
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
165
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
166
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
167
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
168
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
169
+ SOFTWARE.
170
+
171
+ This notice is published in Arabic and English. Where the two differ, the
172
+ Arabic version governs.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @quran.ws/qvp-react-native
2
2
 
3
3
  React Native wrapper for the QVP vector mushaf engine. Android-first: a native view
4
- (`<QvpPageView />`) hosting the Kotlin library's `QvpPageView` (`net.quranpedia.qvp`,
4
+ (`<QvpPageView />`) hosting the Kotlin library's `QvpPageView` (`ws.quran.qvp`,
5
5
  `packages/android/qvp`, JNI over `crates/qvp-ffi/include/qvp.h`) plus a promise-based module
6
6
  (`QvpModule`) for queries. The wrapper is *thin*: no hit-testing, layout or styling logic in JS or
7
7
  in the module — it marshals props/targets/selectors and reconciles declarative arrays against engine
@@ -37,7 +37,7 @@ URIs: `asset://pages/042.qvp`, `file:///…`, `/abs/path`, or `base64:…`.
37
37
  ## The view
38
38
 
39
39
  ```tsx
40
- import { QvpPageView, useQvp, Sel, T, LAYER, KIND, DECO, rgba } from '@quran.ws/qvp-react-native';
40
+ import { QvpPageView, useQvp, Sel, T, LAYER, KIND, DECORATION, rgba } from '@quran.ws/qvp-react-native';
41
41
 
42
42
  const qvp = useQvp();
43
43
  <QvpPageView
@@ -46,12 +46,13 @@ const qvp = useQvp();
46
46
  pageUri="asset://pages/042.qvp"
47
47
  wordsUri="asset://pages/042.words.json" // optional sidecar (rasm_imlai/qpc/rasm/search forms)
48
48
  padTop={12} padBottom={12} padSide={8} // dp
49
- lineSpacing={1} lineGap={0} fillHeight={false} // engine layout knobs (page units for lineGap)
49
+ lineSpacing={1} fillHeight={false} // engine layout knobs
50
+ // spacing only opens up: lineSpacing < 1 is clamped to "as printed"
50
51
  paperColor="#fffdf7" defaultInk="#231f20"
51
52
  theme={{ diacritics: '#1a73e8', dots: '#c62828', waqf: '#0a7d32', ms: 200 }} // page.theme(...) — one handle
52
53
  styles={[
53
54
  { id: 'hide-marks', selector: Sel.kind(KIND.MARK), hide: true }, // page.hide(sel)
54
- { id: 'gold', selector: Sel.deco(DECO.AYAH_MARK), color: '#b8860b', ms: 300, layer: LAYER.THEME + 1 },
55
+ { id: 'gold', selector: Sel.decoration(DECORATION.AYAH_MARK), color: '#b8860b', ms: 300, layer: LAYER.THEME + 1 },
55
56
  { id: 'mark', selector: Sel.wordMark(12, 1), color: '#ef6c00', ms: 200, layer: LAYER.TOP }, // 2nd diacritic of word 12
56
57
  { id: 'ayah', target: '2:255', color: '#0a7d32' }, // page.styleTarget(...)
57
58
  ]}
@@ -61,31 +62,31 @@ const qvp = useQvp();
61
62
  ]}
62
63
  mask={{ target: '2:255', mode: 'hide' }} // or 'block' | 'blur' (+ blockColor, padX, padY, radius)
63
64
  reveal={{ lit: 2, grey: '#c9c4b8', ink: '#231f20', ms: 150, at: 3 }} // greyed page; `at` drives revealGoto
64
- onWordTap={({ word, hit }) => …} // word: {idx, surah, ayah, word, line, …, text, wordKey, aid, forms, label, paths[]}
65
- onDecoTap={({ deco, hit }) => …} // ayah markers, banners, basmalah, rosettes, sajdah signs
65
+ onWordTap={({ word, hit }) => …} // word: {index, surah, ayah, word, line, …, text, wordKey, aid, forms, label, paths[]}
66
+ onDecorationTap={({ decoration, hit }) => …} // ayah markers, banners, basmalah, rosettes, sajdah signs
66
67
  onEmptyTap={() => …}
67
68
  onSelectionChanged={({ words, text, citation, textWithCitation }) => …} // long-press-drag selection
68
- onPageLoad={info => …} // {page, width, height, nLines, nAyahs, nWords, nPaths, nDecos, forms, loadMs, bytes}
69
+ onPageLoad={info => …} // {page, width, height, nLines, nAyahs, nWords, nPaths, nDecorations, forms, loadMs, bytes}
69
70
  onRevealChanged={({ steps, at }) => …}
70
71
  onError={({ message }) => …}
71
72
  />
72
73
  ```
73
74
 
74
75
  Reconciliation: each `highlights` entry maps to one engine handle. A new id → `highlight`; a changed
75
- `target` → `rehighlight` (the band slides); a changed `style` → `restyleHighlight`; a removed id →
76
- `unhighlight` (fades out). `styles` works the same over `style` / `styleTarget` / `hide` / `restyle` /
77
- `unstyle`; `theme` is one handle; `mask` re-applies `maskOptions` + `mask` when the object changes
76
+ `target` → `moveHighlight` (the band slides); a changed `style` → `restyleHighlight`; a removed id →
77
+ `removeHighlight` (fades out). `styles` works the same over `style` / `styleTarget` / `hide` / `recolorStyle` /
78
+ `removeStyle`; `theme` is one handle; `mask` re-applies `maskOptions` + `mask` when the object changes
78
79
  (`null` → `unmask`); `reveal` calls `revealStart` when its config changes and `revealGoto` when `at`
79
80
  changes (`null` → `revealStop`). When `pageUri` changes, the old page is freed and every declarative
80
81
  prop is re-applied to the new page.
81
82
 
82
- Gestures are the Kotlin view's: tap → gap-aware `hitTestViewEx` → `onWordTap` / `onDecoTap` /
83
+ Gestures are the Kotlin view's: tap → gap-aware `hitTestView` → `onWordTap` / `onDecorationTap` /
83
84
  `onEmptyTap`; long-press-drag → engine `select` with a band in `LAYER.SELECTION`; pinch / pan;
84
85
  double-tap resets. Props `selectionEnabled`, `zoomEnabled`, `hitMaxDistance`, `selectionBand`.
85
86
 
86
87
  Colours: `'#rgb' | '#rrggbb' | '#rrggbbaa' | 0xRRGGBBAA` (`css()` / `rgba(c, alpha)` helpers).
87
- Targets: `'page' | '2:255' | '2:255:3' | '2:255-257' | 'line:7' | 'surah:2' | wordIdx | [idx…] | T.*`.
88
- Selectors: `Sel.page/path/wordPath/wordMark/wordMarkNamed/wordBody/wordMarks/word/ayah/line/mark/category/family/kind/deco/decoIdx`.
88
+ Targets: `'page' | '2:255' | '2:255:3' | '2:255-257' | 'line:7' | 'surah:2' | wordIdx | [index…] | T.*`.
89
+ Selectors: `Sel.page/path/wordPath/wordMark/wordMarkNamed/wordBody/wordMarks/word/ayah/line/mark/category/family/kind/decoration/decorationIndex`.
89
90
 
90
91
  ## The module
91
92
 
@@ -94,42 +95,43 @@ Everything takes the view's react tag (`findNodeHandle`) — or use the bound fo
94
95
 
95
96
  ```ts
96
97
  const qvp = useQvp();
97
- await qvp.info(); qvp.words(); qvp.word(i); qvp.ayahs(); qvp.lines(); qvp.decos();
98
+ await qvp.info(); qvp.words(); qvp.word(i); qvp.ayahs(); qvp.lines(); qvp.decorations();
98
99
  await qvp.search('الرحمان', { mode: 'includes' }); // [{word, wordKey, text, index, loose}]
99
100
  await qvp.text('2:255', { form: 'search', wordSep: ' ' });
100
- await qvp.resolve('line:7'); qvp.findWord(2, 255, 3); qvp.wordForm(i, 'rasm_imlai'); qvp.hasForm('qpc');
101
+ await qvp.targetWords('line:7'); qvp.findWord(2, 255, 3); qvp.wordForm(i, 'rasm_imlai'); qvp.hasForm('qpc');
101
102
  await qvp.attachWords(jsonString); // when you do not use the wordsUri prop
102
- await qvp.surahs(); qvp.divisions(); qvp.markers(); qvp.rosettes(); qvp.sajdahs(); qvp.ayahKeys();
103
- await qvp.ayahWordCount(2, 255); qvp.reciteMap(2, 255, 4); qvp.wordLabel(i); qvp.ayahLabel(ai);
103
+ await qvp.surahs(); qvp.divisions(); qvp.ayahMarks(); qvp.rosettes(); qvp.sajdahs(); qvp.ayahKeys();
104
+ await qvp.ayahWordCount(2, 255); qvp.reciteMap(2, 255, 4); qvp.wordLabel(i); qvp.ayahLabel(ayahIndex);
104
105
  await qvp.citation([12, 13, 14]);
105
- await qvp.cropSvg('2:255', { pad: 3, keepMarkers: true, background: '#fffdf7' }); qvp.cropBox(target);
106
+ await qvp.cropSvg('2:255', { pad: 3, keepMarkers: true, background: '#fffdf7' }); qvp.cropBounds(target);
106
107
  await qvp.select(anchor, focus); qvp.clearSelection(); qvp.selection(); qvp.selectionText('rasm_uthmani', true);
107
- await qvp.revealNext(1); qvp.hideBack(1); qvp.revealWord(i); qvp.hideWord(i); qvp.revealAll(); qvp.hideAll();
108
- await qvp.maskHidden(); qvp.maskWords(); qvp.revealSteps(); qvp.revealAt(); qvp.revealStepOf(i);
109
- await qvp.hitTestViewEx(x, y, { maxDistance: 6 }); qvp.wordBoxView(i); qvp.currentLayout(); qvp.relayout(); qvp.resetView(); qvp.stats();
108
+ await qvp.unmaskNext(1); qvp.maskBack(1); qvp.unmaskWord(i); qvp.maskWord(i); qvp.unmaskAll(); qvp.maskAll();
109
+ await qvp.maskHidden(); qvp.maskWords(); qvp.revealStepCount(); qvp.revealPosition(); qvp.revealStepOf(i);
110
+ await qvp.hitTestView(x, y, { maxDistance: 6 }); qvp.hitTestExactView(x, y); qvp.hitTest(px, py); qvp.hitTestExact(px, py); // view dp or page units
111
+ await qvp.wordBoundsView(i); qvp.currentLayout(); qvp.layoutLineSpacingToFill(); qvp.relayout(); qvp.resetView(); qvp.stats();
110
112
 
111
113
  // engine-wide (Qvp.*)
112
114
  await Qvp.strip(s); Qvp.fold(s); Qvp.normalize(s); Qvp.looseKey(s); // = Qvp.arabic(kind, s)
113
- await Qvp.gapToFill(pageW, pageH, lines, viewW, viewH); Qvp.wastedFraction(...)
114
- Qvp.markName(7); Qvp.kindName(1); Qvp.categoryName(1); Qvp.familyName(3); Qvp.version
115
+ await qvp.layoutLineSpacingToFill(); qvp.layoutWastedFraction(); qvp.grid()
116
+ Qvp.markName(7); Qvp.kindName(1); Qvp.categoryName(1); Qvp.familyName(3); Qvp.nameId('marks', 'fathah'); Qvp.version; await Qvp.markCategory(7); Qvp.engineName()
115
117
 
116
118
  // atlas (cross-page)
117
119
  const atlas = await QvpAtlas.load('asset://pages/atlas.qva');
118
120
  await atlas.pageOf(2, 255); atlas.pageRange(42); atlas.surah(36); atlas.surahs(); atlas.pageOfSurah(36);
119
- await atlas.juz(30); atlas.hizb(3); atlas.rubuAlHizb(7); atlas.juzAt(2, 255); atlas.divisionAt('hizb', 2, 255);
120
- await atlas.pagesOfJuz(30); atlas.findSurah('cow' | 'البقرة' | '2'); atlas.free();
121
- // tag-level equivalents: Qvp.atlasPageOf(id, s, a), Qvp.atlasFindSurah(id, text), Qvp.atlasPagesOfJuz(id, n), Qvp.atlasJuzAt(id, s, a), …
121
+ await atlas.juz(30); atlas.hizb(3); atlas.rubuAlHizb(7); atlas.juzOf(2, 255); atlas.divisionOf('hizb', 2, 255);
122
+ await atlas.pagesOfJuz(30); atlas.searchSurahs('cow' | 'البقرة' | '2'); atlas.free();
123
+ // tag-level equivalents: Qvp.atlasPageOf(id, s, a), Qvp.atlasSearchSurahs(id, text), Qvp.atlasPagesOfJuz(id, n), Qvp.atlasJuzOf(id, s, a), …
122
124
  ```
123
125
 
124
126
  All page calls run on the UI thread (where the view draws), so the engine is never touched from two
125
- threads. Only `maskHidden`, `unmask` (via the `mask` prop), `revealNext` etc. mutate state; queries are
127
+ threads. Only `maskHidden`, `unmask` (via the `mask` prop), `unmaskNext` etc. mutate state; queries are
126
128
  microseconds.
127
129
 
128
130
  ## Layout of this package
129
131
 
130
132
  ```
131
133
  android/build.gradle library module; depends on project(':qvp') or compiles its sources
132
- android/src/main/java/net/quranpedia/qvp/rn/
134
+ android/src/main/java/ws/quran/qvp/rn/
133
135
  QvpRnPageView.kt QvpPageView subclass: page loading, prop → handle reconciliation, events
134
136
  QvpPageViewManager.kt SimpleViewManager<QvpRnPageView> (props, events, commit after each prop batch)
135
137
  QvpModule.kt ReactContextBaseJavaModule: page queries by react tag, engine helpers, atlas
@@ -1,5 +1,5 @@
1
1
  // @quran.ws/qvp-react-native — Android module. Thin: marshalling + handle reconciliation over the Kotlin
2
- // library `net.quranpedia.qvp` (packages/android/qvp), which is JNI over crates/qvp-ffi/include/qvp.h.
2
+ // library `ws.quran.qvp` (packages/android/qvp), which is JNI over crates/qvp-ffi/include/qvp.h.
3
3
  //
4
4
  // Linking the Kotlin library, two ways:
5
5
  // 1. (preferred) the app's settings.gradle includes it as a Gradle project:
@@ -25,7 +25,7 @@ def qvpProject = findProject(":qvp")
25
25
  def qvpDir = file(rootProject.findProperty("qvpAndroidDir") ?: "$projectDir/../../../android/qvp")
26
26
 
27
27
  android {
28
- namespace "net.quranpedia.qvp.rn"
28
+ namespace "ws.quran.qvp.rn"
29
29
  compileSdk safeExtGet("compileSdkVersion", 35)
30
30
  defaultConfig {
31
31
  minSdkVersion safeExtGet("minSdkVersion", 24)
@@ -1,20 +1,20 @@
1
- package net.quranpedia.qvp.rn
1
+ package ws.quran.qvp.rn
2
2
 
3
3
  import com.facebook.react.bridge.Arguments
4
4
  import com.facebook.react.bridge.ReadableArray
5
5
  import com.facebook.react.bridge.ReadableMap
6
6
  import com.facebook.react.bridge.WritableArray
7
7
  import com.facebook.react.bridge.WritableMap
8
- import net.quranpedia.qvp.*
9
- import net.quranpedia.qvp.Target
8
+ import ws.quran.qvp.*
9
+ import ws.quran.qvp.Target
10
10
 
11
11
  /**
12
12
  * Marshalling only: JS shapes (docs/API.md) ↔ Kotlin library types. No hit-testing, layout or
13
13
  * styling decisions live here — every call goes to [QvpPage] / [QvpAtlas], which go to the engine.
14
14
  *
15
15
  * Targets from JS: "page" | "2:255" | "2:255:3" | "2:255-257" | "line:7" | "surah:2" | word index |
16
- * [word indices] | {kind, a, b, c, words} (kind = QVP_TARGET_* or its name).
17
- * Selectors from JS: {kind, a, b, c} (kind = QVP_SEL_* or its name). Colours: '#rgb' | '#rrggbb' | '#rrggbbaa'.
16
+ * [word indices] | {target, a, b, c, words} (target = QVP_TARGET_* or its name).
17
+ * Selectors from JS: {selector, a, b, c} (selector = QVP_SELECTOR_* or its name). Colours: '#rgb' | '#rrggbb' | '#rrggbbaa'.
18
18
  */
19
19
  object Marshal {
20
20
  // ── colours ──
@@ -37,7 +37,7 @@ object Marshal {
37
37
  }
38
38
 
39
39
  private val TARGET_KINDS = listOf("page", "word", "words", "ayah", "ayahRange", "line", "surah", "range")
40
- private val SEL_KINDS = listOf("page", "path", "wordPath", "wordMark", "wordMarkNamed", "wordBody", "wordMarks", "word", "ayah", "line", "mark", "category", "family", "kind", "deco", "decoIdx")
40
+ private val SEL_KINDS = listOf("page", "path", "wordPath", "wordMark", "wordMarkNamed", "wordBody", "wordMarks", "word", "ayah", "line", "mark", "category", "family", "kind", "decoration", "decorationIndex")
41
41
  private fun kindOf(v: Any?, names: List<String>): Int = when (v) {
42
42
  is Number -> v.toInt()
43
43
  is String -> names.indexOf(v).let { if (it >= 0) it else v.toIntOrNull() ?: -1 }
@@ -53,7 +53,7 @@ object Marshal {
53
53
  is IntArray -> Target.words(v)
54
54
  is Map<*, *> -> {
55
55
  val a = num(v["a"]) ?: 0; val b = num(v["b"]) ?: 0; val c = num(v["c"]) ?: 0
56
- when (kindOf(v["kind"], TARGET_KINDS)) {
56
+ when (kindOf(v["target"], TARGET_KINDS)) {
57
57
  0 -> Target.page(); 1 -> Target.word(a); 2 -> Target.words(ints(v["words"] ?: v["a"]))
58
58
  3 -> Target.ayah(a, b); 4 -> Target.ayahRange(a, b, c); 5 -> Target.line(a); 6 -> Target.surah(a); 7 -> Target.range(a, b)
59
59
  else -> Target.words(IntArray(0))
@@ -62,16 +62,16 @@ object Marshal {
62
62
  else -> Target.words(IntArray(0))
63
63
  }
64
64
 
65
- /** JS selector {kind, a, b, c} → [Selector], or null when malformed. */
65
+ /** JS selector {selector, a, b, c} → [Selector], or null when malformed. */
66
66
  fun selector(v: Any?): Selector? {
67
67
  val m = v as? Map<*, *> ?: return null
68
68
  val a = num(m["a"]) ?: 0; val b = num(m["b"]) ?: 0; val c = num(m["c"]) ?: 0
69
- return when (kindOf(m["kind"], SEL_KINDS)) {
69
+ return when (kindOf(m["selector"], SEL_KINDS)) {
70
70
  0 -> Selector.page(); 1 -> Selector.path(a); 2 -> Selector.wordPath(a, b); 3 -> Selector.wordMark(a, b)
71
- 4 -> Selector.wordMarkNamed(a, (m["mark"] as? String) ?: QVP_MARKS.getOrElse(b) { "" }, c)
71
+ 4 -> Selector.wordMarkNamed(a, (m["mark"] as? String) ?: QvpEngine.markName(b), c)
72
72
  5 -> Selector.wordBody(a); 6 -> Selector.wordMarks(a); 7 -> Selector.word(a); 8 -> Selector.ayah(a, b); 9 -> Selector.line(a)
73
73
  10 -> (m["mark"] as? String)?.let { Selector.mark(it) } ?: Selector.mark(a)
74
- 11 -> Selector.category(a); 12 -> Selector.family(a); 13 -> Selector.kind(a); 14 -> Selector.deco(a); 15 -> Selector.decoIdx(a)
74
+ 11 -> Selector.category(a); 12 -> Selector.family(a); 13 -> Selector.kind(a); 14 -> Selector.decoration(a); 15 -> Selector.decorationIndex(a)
75
75
  else -> null
76
76
  }
77
77
  }
@@ -117,40 +117,41 @@ object Marshal {
117
117
  // ── Kotlin → JS ──
118
118
  fun word(p: QvpPage, w: QvpWord): Map<String, Any?> {
119
119
  val forms = LinkedHashMap<String, String>()
120
- for (f in Form.entries) if (f == Form.RASM_UTHMANI || p.hasForm(f)) forms[f.name.lowercase()] = p.wordForm(w.idx, f)
120
+ for (f in Form.entries) if (f == Form.RASM_UTHMANI || p.hasForm(f)) forms[f.name.lowercase()] = p.wordForm(w.index, f)
121
121
  val paths = (w.firstPath until w.firstPath + w.nPaths).map { i ->
122
122
  val kind = p.pathKind(i); val mark = p.pathMark(i); val cat = p.pathCategory(i); val fam = p.pathFamily(i)
123
- mapOf("idx" to i, "kind" to kind, "kindName" to QvpEngine.kindName(kind), "mark" to mark, "markName" to QvpEngine.markName(mark), "nthMark" to p.pathNthMark(i),
123
+ mapOf("index" to i, "kind" to kind, "kindName" to QvpEngine.kindName(kind), "mark" to mark, "markName" to QvpEngine.markName(mark), "nthMark" to p.pathNthMark(i),
124
124
  "nthInWord" to p.pathNthInWord(i), "category" to cat, "categoryName" to QvpEngine.categoryName(cat), "family" to fam, "familyName" to QvpEngine.familyName(fam), "line" to p.pathLine(i))
125
125
  }
126
- return mapOf("idx" to w.idx, "surah" to w.surah, "ayah" to w.ayah, "word" to w.word, "line" to w.line, "lineIdx" to w.lineIdx, "ayahIdx" to w.ayahIdx,
126
+ return mapOf("index" to w.index, "surah" to w.surah, "ayah" to w.ayah, "word" to w.word, "line" to w.line, "lineIndex" to w.lineIndex, "ayahIndex" to w.ayahIndex,
127
127
  "x0" to w.x0, "y0" to w.y0, "x1" to w.x1, "y1" to w.y1, "text" to w.text, "firstPath" to w.firstPath, "nPaths" to w.nPaths,
128
- "wordKey" to w.wordKey, "ayahKey" to w.ayahKey, "forms" to forms, "label" to p.wordLabel(w.idx), "paths" to paths)
128
+ "wordKey" to w.wordKey, "ayahKey" to w.ayahKey, "forms" to forms, "label" to p.wordLabel(w.index), "paths" to paths)
129
129
  }
130
- fun deco(d: QvpDecoration): Map<String, Any?> = mapOf("idx" to d.idx, "kind" to d.kind, "kindName" to listOf("ayah-ayahMark", "surah-name", "basmalah", "division-mark", "sajdah-mark").getOrElse(d.kind) { "other" },
130
+ fun decoration(d: QvpDecoration): Map<String, Any?> = mapOf("index" to d.index, "decoration" to d.decoration, "decorationName" to QvpEngine.decorationName(d.decoration).ifEmpty { "other" },
131
131
  "surah" to d.surah, "ayah" to d.ayah, "line" to d.line, "x0" to d.x0, "y0" to d.y0, "x1" to d.x1, "y1" to d.y1, "text" to d.text, "firstPath" to d.firstPath, "nPaths" to d.nPaths)
132
- fun hit(p: QvpPage, h: QvpHitEx): Map<String, Any?> {
132
+ fun hit(p: QvpPage, h: QvpHit): Map<String, Any?> {
133
133
  val w = if (h.word >= 0) p.words[h.word] else null
134
- return mapOf("word" to h.word, "path" to h.path, "deco" to h.deco, "line" to h.line, "distance" to h.distance, "exact" to h.exact, "wordKey" to w?.wordKey, "ayahKey" to w?.ayahKey)
134
+ return mapOf("word" to h.word, "path" to h.path, "decoration" to h.decoration, "line" to h.line, "distance" to h.distance, "isExact" to h.isExact, "wordKey" to w?.wordKey, "ayahKey" to w?.ayahKey)
135
135
  }
136
- fun ayah(a: QvpAyah): Map<String, Any?> = mapOf("idx" to a.idx, "surah" to a.surah, "ayah" to a.ayah, "fragment" to a.fragment, "fragments" to a.fragments, "flags" to a.flags, "rubuAlHizb" to a.rubuAlHizb, "firstWord" to a.firstWord, "nWords" to a.nWords,
137
- "ayahMarkDeco" to a.ayahMarkDeco, "bbox" to listOf(a.x0, a.y0, a.x1, a.y1))
138
- fun line(l: QvpLine): Map<String, Any?> = mapOf("idx" to l.idx, "lineNo" to l.lineNo, "isHeader" to l.isHeader, "firstWord" to l.firstWord, "nWords" to l.nWords, "bbox" to listOf(l.x0, l.y0, l.x1, l.y1),
136
+ fun ayah(a: QvpAyah): Map<String, Any?> = mapOf("index" to a.index, "surah" to a.surah, "ayah" to a.ayah, "fragment" to a.fragment, "fragments" to a.fragments, "flags" to a.flags, "rubuAlHizb" to a.rubuAlHizb, "firstWord" to a.firstWord, "nWords" to a.nWords,
137
+ "ayahMarkDecoration" to a.ayahMarkDecoration, "bbox" to listOf(a.x0, a.y0, a.x1, a.y1))
138
+ fun line(l: QvpLine): Map<String, Any?> = mapOf("index" to l.index, "lineNumber" to l.lineNumber, "isHeader" to l.isHeader, "firstWord" to l.firstWord, "nWords" to l.nWords, "bbox" to listOf(l.x0, l.y0, l.x1, l.y1),
139
139
  "bandY0" to l.bandY0, "bandY1" to l.bandY1, "centre" to l.centre)
140
- fun surah(s: QvpSurah): Map<String, Any?> = mapOf("number" to s.number, "ayahCount" to s.ayahCount, "hasBanner" to s.hasBanner, "hasBasmalah" to s.hasBasmalah, "place" to s.place, "bannerDeco" to s.bannerDeco,
140
+ fun surah(s: QvpSurah): Map<String, Any?> = mapOf("number" to s.number, "ayahCount" to s.ayahCount, "hasBanner" to s.hasBanner, "hasBasmalah" to s.hasBasmalah, "place" to s.place, "bannerDecoration" to s.bannerDecoration,
141
141
  "arabic" to s.arabic, "latin" to s.latin, "english" to s.english)
142
- fun division(d: QvpDivision): Map<String, Any?> = mapOf("kind" to d.kind.name.lowercase(), "n" to d.n, "surah" to d.surah, "ayah" to d.ayah, "line" to d.line, "ayahIdx" to d.ayahIdx)
143
- fun ayahMark(m: QvpAyahMark): Map<String, Any?> = mapOf("deco" to m.deco, "surah" to m.surah, "ayah" to m.ayah, "line" to m.line, "cx" to m.cx, "cy" to m.cy, "r" to m.r, "ornamentPath" to m.ornamentPath, "numeralPath" to m.numeralPath)
144
- fun rosette(r: QvpRosette): Map<String, Any?> = mapOf("deco" to r.deco, "surah" to r.surah, "ayah" to r.ayah, "juz" to r.juz, "hizb" to r.hizb, "nisf" to r.nisf, "rubuAlHizb" to r.rubuAlHizb, "rubuAlHizbInHizb" to r.rubuAlHizbInHizb)
145
- fun sajdah(s: QvpSajdah): Map<String, Any?> = mapOf("deco" to s.deco, "surah" to s.surah, "ayah" to s.ayah, "signPath" to s.signPath)
146
- fun match(m: QvpMatch): Map<String, Any?> = mapOf("word" to m.word, "index" to m.index, "loose" to m.loose, "wordKey" to m.wordKey, "text" to m.text)
147
- fun cropBox(c: QvpCropBox): Map<String, Any?> = mapOf("x0" to c.x0, "y0" to c.y0, "x1" to c.x1, "y1" to c.y1, "nWords" to c.nWords, "ayahMarkDeco" to c.ayahMarkDeco)
148
- fun atlasSurah(s: QvpAtlasSurah): Map<String, Any?> = mapOf("n" to s.n, "number" to s.n, "page" to s.page, "ayahCount" to s.ayahCount, "place" to s.place, "arabic" to s.arabic, "latin" to s.latin, "english" to s.english)
142
+ fun division(d: QvpDivision): Map<String, Any?> = mapOf("division" to d.division.name.lowercase(), "number" to d.number, "surah" to d.surah, "ayah" to d.ayah, "line" to d.line, "ayahIndex" to d.ayahIndex)
143
+ fun surahHeader(h: QvpSurahHeader): Map<String, Any?> = mapOf("decoration" to h.decoration, "surah" to h.surah, "line" to h.line, "x0" to h.x0, "y0" to h.y0, "x1" to h.x1, "y1" to h.y1, "titleX0" to h.titleX0, "titleY0" to h.titleY0, "titleX1" to h.titleX1, "titleY1" to h.titleY1)
144
+ fun ayahMark(m: QvpAyahMark): Map<String, Any?> = mapOf("decoration" to m.decoration, "surah" to m.surah, "ayah" to m.ayah, "line" to m.line, "cx" to m.cx, "cy" to m.cy, "r" to m.r, "ornamentPath" to m.ornamentPath, "numeralPath" to m.numeralPath)
145
+ fun rosette(r: QvpRosette): Map<String, Any?> = mapOf("decoration" to r.decoration, "surah" to r.surah, "ayah" to r.ayah, "juz" to r.juz, "hizb" to r.hizb, "nisf" to r.nisf, "rubuAlHizb" to r.rubuAlHizb, "rubuAlHizbInHizb" to r.rubuAlHizbInHizb)
146
+ fun sajdah(s: QvpSajdah): Map<String, Any?> = mapOf("decoration" to s.decoration, "surah" to s.surah, "ayah" to s.ayah, "signPath" to s.signPath)
147
+ fun match(m: QvpMatch): Map<String, Any?> = mapOf("word" to m.word, "index" to m.index, "isLooseMatch" to m.isLooseMatch, "wordKey" to m.wordKey, "text" to m.text)
148
+ fun cropBounds(c: QvpCropBounds): Map<String, Any?> = mapOf("x0" to c.x0, "y0" to c.y0, "x1" to c.x1, "y1" to c.y1, "nWords" to c.nWords, "ayahMarkDecoration" to c.ayahMarkDecoration)
149
+ fun atlasSurah(s: QvpAtlasSurah): Map<String, Any?> = mapOf("number" to s.number, "page" to s.page, "ayahCount" to s.ayahCount, "place" to s.place, "arabic" to s.arabic, "latin" to s.latin, "english" to s.english)
149
150
  fun atlasRubuAlHizb(r: QvpAtlasRubuAlHizb): Map<String, Any?> = mapOf("rubuAlHizb" to r.rubuAlHizb, "surah" to r.surah, "ayah" to r.ayah, "page" to r.page, "ayahKey" to r.ayahKey)
150
- fun layout(l: QvpLayout): Map<String, Any?> = mapOf("scale" to l.scale, "ox" to l.ox, "oy" to l.oy, "contentW" to l.contentW, "contentH" to l.contentH, "pitch" to l.pitch, "lineDy" to l.lineDy.toList(),
151
+ fun layout(l: QvpLayout): Map<String, Any?> = mapOf("scale" to l.scale, "offsetX" to l.offsetX, "offsetY" to l.offsetY, "contentW" to l.contentW, "contentH" to l.contentH, "lineSpacing" to l.lineSpacing, "fitScale" to l.fitScale, "fitX" to l.fitX, "fitY" to l.fitY, "lineDy" to l.lineDy.toList(),
151
152
  "slots" to l.slotTop.indices.map { listOf(l.slotTop[it], l.slotBottom[it]) })
152
- fun pageInfo(p: QvpPage): Map<String, Any?> = mapOf("page" to p.pageNo, "width" to p.width, "height" to p.height, "nLines" to p.nLines, "nAyahs" to p.nAyahs, "nWords" to p.nWords, "nPaths" to p.nPaths, "nDecos" to p.nDecos,
153
- "naturalPitch" to p.naturalPitch, "forms" to Form.entries.filter { it == Form.RASM_UTHMANI || p.hasForm(it) }.map { it.name.lowercase() })
153
+ fun pageInfo(p: QvpPage): Map<String, Any?> = mapOf("page" to p.pageNo, "width" to p.width, "height" to p.height, "nLines" to p.nLines, "nAyahs" to p.nAyahs, "nWords" to p.nWords, "nPaths" to p.nPaths, "nDecorations" to p.nDecorations,
154
+ "lineSpacing" to p.lineSpacing, "forms" to Form.entries.filter { it == Form.RASM_UTHMANI || p.hasForm(it) }.map { it.name.lowercase() })
154
155
  fun selection(p: QvpPage): Map<String, Any?> {
155
156
  val ws = p.selection()
156
157
  return mapOf("words" to ws.toList(), "text" to (if (ws.isEmpty()) "" else p.text(Target.words(ws))), "citation" to (if (ws.isEmpty()) "" else p.citation(ws)),