Support multiple riwayat
A riwayah is not a display setting. It changes the letters, the word count, the font, the ayah count and where ayahs begin and end. This guide makes the switch safe: everything that can change with the reading is read from the reading, and nothing that was true under one is assumed under another.
If you only ever ship Ḥafṣ, Display Qur'an text is enough, plus one habit from here: never write a count as a literal.
What changes when the reading changes
Seven editions ship, each a printed muṣḥaf in one riwayah, on its own terms:
| Key | Riwayah | Counting system | Ayat in this muṣḥaf | System total |
|---|---|---|---|---|
| hafs | Ḥafṣ ʿan ʿĀṣim | Kufi | 6,236 | 6,236 |
| shubah | Shuʿbah ʿan ʿĀṣim | Kufi | 6,236 | 6,236 |
| warsh | Warsh ʿan Nāfiʿ | Last Madani | 6,214 | 6,214 |
| qalun | Qālūn ʿan Nāfiʿ | Last Madani | 6,214 | 6,214 |
| duri | al-Dūrī ʿan Abī ʿAmr | First Madani | 6,217 | 6,214differs |
| susi | al-Sūsī ʿan Abī ʿAmr | First Madani | 6,218 | 6,214differs |
| bazzi | al-Bazzī ʿan Ibn KathīrBeta | Makki | 6,220 | 6,219differs |
Muṣḥaf counts come from quran-text’s own files; system totals from qiraat-ayah-map. Both are read at build time. Where they differ, both numbers are correct about different questions — see below.
Four things move at once when the user picks another row: the text (real letters differ, and
19,431 Warsh words use code points only its own font draws), the font, the ayah count and
the ayah boundaries, so a stored 2:255 means a different passage in a different edition.
By the end, the ayah picker's range, the font, the cache and the bookmarks all follow the riwayah switch, and a bookmark taken in Ḥafṣ opens the right verse in Warsh.
Load a riwayah as its own dataset
Ḥafṣ is bundled; the others are files. Load the one selected, and keep the loaded object as the single source of everything below.
import { Mushaf } from "quran-text"; const EDITIONS = ["hafs", "shubah", "warsh", "qalun", "duri", "susi", "bazzi"]; async function loadRiwayah(key) { if (key === "hafs") return Mushaf.hafs(); const res = await fetch(`/data/mushaf/${key}.json`); // 1.9–3.1 MB, cache it return Mushaf.fromJson(await res.json()); } let current = await loadRiwayah("warsh"); current.key; // "warsh" current.nameEn; // "Warsh" current.ayahCount; // 6214 current.wordCount; // 77431Choose the riwayah when you load, and treat
currentas replaced, not mutated, on a switch.Read every count from the loaded edition
current.ayahCount; // 6214 for Warsh, 6236 for Ḥafṣ, 6218 for al-Sūsī current.surah(2).ayahCount; // 285 for Warsh, 286 for Ḥafṣ current.surah(1).ayahCount; // 7 in all seven — and still not the same seven boundariesBind the ayah picker's maximum to
current.surah(n).ayahCount, and any "loop over the muṣḥaf" tocurrent.ayahCount. Both change on a switch; both are already in memory.6,236 is not a constantIt is the Kufan total, which Ḥafṣ follows.
for (a = 1; a <= 6236; …)is correct until the first user selects Warsh, and then it overruns by 22. Two further traps sit in the table above: a muṣḥaf’s printed count and its counting system’s published total can disagree (al-Sūsī prints 6,218 against a system total of 6,214), and First and Last Madinan share a total while placing boundaries differently. Only the loaded file is right about the loaded file.Switch the font with the text
Each edition names its own font, and the names differ. Register all seven once, and set the family from the loaded edition:
current.font.family; // "KFGQPC Warsh Uthmanic Script" current.font.file; // "UthmanicWarsh-v-3.0.ttf" — in the dataset's data/fonts/// once, for every edition you ship — fontFace(url) needs the URL for anything but Ḥafṣ const css = (await Promise.all(EDITIONS.map(loadRiwayah))) .map((m) => m.fontFace(`/fonts/${m.font.file}`)) .join("\n"); document.head.append(Object.assign(document.createElement("style"), { textContent: css })); // on every switch root.style.fontFamily = `"${current.font.family}"`;One file per edition, not one for all seven. In the wrong font Warsh, Qālūn and al-Sūsī show empty boxes where letters should be. Two details:
fontFace()called with no URL on a non-bundled edition writesurl("null")into the rule, andfontFace(url)always writesformat("truetype"), so pass a.ttfURL, or write the rule yourself for WOFF2.Key every cache on the riwayah
Anything you cache under a reference — rendered HTML, a fetched translation, an audio segment, a page image — was computed for one edition. Put the edition key in the cache key, and the source package's digest if you cache to disk, so a dataset update invalidates it too; a cache key names everything the value depends on (Engineering, rule 8.1):
const cacheKey = (m, ref) => `${m.key}:${m.provenance.text.sha256.slice(0, 12)}:${ref}`; cacheKey(current, "2:253"); // "warsh:39cd7af1ab0e:2:253" ← illustrative; read the real digest from m.provenancem.provenance.textnames the publisher's package and its SHA-256 for every edition; it is the identity of the text you rendered. A cache keyed on the reference alone serves Ḥafṣ text under a Warsh label the moment the user switches.Persist references in one numbering, and translate on the way out
A reference saved as
2:255cannot be restored after a switch. Save it with the edition it was taken in, and translate when you display it. Both directions are one call, and the result says how the two editions relate:const hafs = await loadRiwayah("hafs"); const warsh = await loadRiwayah("warsh"); hafs.ayah(1, 4).to(warsh).key; // "1:3" relation "same" — one ayah earlier hafs.ayah(2, 255).to(warsh).key; // "2:253-254" relation "split" — one Ḥafṣ ayah is two here warsh.ayah(2, 253).to(hafs).key; // "2:255" relation "merged" hafs.ayah(1, 1).to(warsh).key; // "" relation "unnumbered" — the basmalah is not an ayah in Warsh hafs.ayah(1, 7).to(warsh).key; // "1:6-7" relation "split"So a bookmark is a triple, and opening it is a translation:
const bookmark = { riwayah: "hafs", surah: 2, ayah: 255 }; function open(bookmark, current, editions) { const from = editions[bookmark.riwayah]; const match = from.ayah(bookmark.surah, bookmark.ayah).to(current); if (match.relation === "unnumbered") return current.surah(bookmark.surah).basmalah; // show the basmalah return match.ayahs; // one or more Ayah objects in the current edition }Handle every
relation(same,merged,split,shifted,unnumbered) and never collapse a split to its first number: that drops the second half of āyat al-Kursī.If you would rather store references in one fixed numbering, use Ḥafṣ's, the Kufan count every mapping goes through, and
data/ayah-map.jsonconverts without loading any edition:import { AyahMap } from "quran-text"; const map = AyahMap.fromJson(await (await fetch("/data/ayah-map.json")).json()); map.convert(2, 255, "warsh"); // { surah: 2, ayah: 253, ayahLast: 254, relation: "split" } map.all(7, 206); // 7:206 "same" in all seven — this one does not movePut the switch together
const editions = {}; async function switchTo(key, bookmark) { editions[key] ??= await loadRiwayah(key); current = editions[key]; root.style.fontFamily = `"${current.font.family}"`; picker.max = current.surah(bookmark.surah).ayahCount; // range follows the edition const shown = open(bookmark, current, editions); // translated, not carried render(shown); // one element per Ayah, keyed by .key caption.textContent = `${current.nameEn} · ${current.ayahCount} ayat`; }Nothing in that function assumes a number. The count, the range, the font and the reference all came from the edition that is on screen.
Look an ayah up, change riwayah, and watch the surah’s ayah count and the text change with it — from the real files.
The basmalah moves too
In Ḥafṣ, Shuʿbah and al-Bazzī the four words of the basmalah are 1:1. In Warsh, Qālūn, al-Dūrī and al-Sūsī they
are printed, unnumbered, before 1:1, and 1:1 is al-ḥamdu lillāh:
hafs.ayah(1, 1).text; // "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ"
warsh.ayah(1, 1).text; // "ࡴ۬لْحَمْدُ لِلهِ رَبِّ ࡴ۬لْعَٰلَمِينَ"
warsh.surah(1).basmalah.text; // "بِسْمِ ࡴ۬للَّهِ ࡴ۬لرَّحْمَٰنِ ࡴ۬لرَّحِيمِ"
current.basmalahCounted; // true or false — the one flag that says which case you are in
hasBasmalah on a surah still tells you whether to draw one; basmalahCounted on the edition tells
you whether it is numbered. Surah 9 is false everywhere.
One assignment is contested, and we have not settled it
quran-text measures the printed al-Sūsī and al-Dūrī muṣḥafs closest to First Madinan (m.countingSystem), while
qiraat-ayah-map assigns their qāriʾ, Abū ʿAmr, to Basran, and quran-text records that too, as
m.countingSystemAssociatedWithQari. For al-Sūsī the two fields differ. If you translate with
.to() or ayah-map.json you get the printed muṣḥaf’s numbering, which is what a reader
holding that muṣḥaf expects; if you translate with Qiraat Ayah Map by transmitter you get Basran.
They disagree on 7:206, among others. This needs a scholar rather than a patch; see
Ayah-counting systems.
Related guides
| You want | Use instead |
|---|---|
| The printed page of another riwayah | Display a complete Muṣḥaf page — five muṣḥafs are vectorised; page numbers do not carry across |
| Translate a reference between scholarly counting systems, by transmitter | Map ayah references between counts |
| Tajwīd in another riwayah | Nothing — Quran Tajweed is Ḥafṣ only |
How this data is made
Each edition is extracted unedited from the King Fahd Complex's own package for that riwayah, with
the package and its SHA-256 recorded in the file. The shared word numbering that makes .to()
possible is derived by the build and asserted to tile exactly across all seven. Which counting system
each printed edition follows is measured from its boundaries, not declared, and that measurement is
what the contested note above reports.