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.

Platforms
Browser · Node · React Native
Time
About 15 minutes

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:

KeyRiwayahCounting systemAyat in this muṣḥafSystem total
hafsḤafṣ ʿan ʿĀṣimKufi6,2366,236
shubahShuʿbah ʿan ʿĀṣimKufi6,2366,236
warshWarsh ʿan NāfiʿLast Madani6,2146,214
qalunQālūn ʿan NāfiʿLast Madani6,2146,214
durial-Dūrī ʿan Abī ʿAmrFirst Madani6,2176,214differs
susial-Sūsī ʿan Abī ʿAmrFirst Madani6,2186,214differs
bazzial-Bazzī ʿan Ibn KathīrBetaMakki6,2206,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.

  1. 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;    // 77431
    

    Choose the riwayah when you load, and treat current as replaced, not mutated, on a switch.

  2. 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 boundaries
    

    Bind the ayah picker's maximum to current.surah(n).ayahCount, and any "loop over the muṣḥaf" to current.ayahCount. Both change on a switch; both are already in memory.

    6,236 is not a constant

    It 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.

  3. 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 writes url("null") into the rule, and fontFace(url) always writes format("truetype"), so pass a .ttf URL, or write the rule yourself for WOFF2.

  4. 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.provenance
    

    m.provenance.text names 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.

  5. Persist references in one numbering, and translate on the way out

    A reference saved as 2:255 cannot 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.json converts 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 move
    
  6. Put 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.

Try it live

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

Careful

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.

You wantUse instead
The printed page of another riwayahDisplay a complete Muṣḥaf page — five muṣḥafs are vectorised; page numbers do not carry across
Translate a reference between scholarly counting systems, by transmitterMap ayah references between counts
Tajwīd in another riwayahNothing — 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.