Getting started

Install, call, read the result.

Two entry points: the full package with the bundled Qurʾān text, and a corpus-free core for callers that supply their own text.

Install and import

npm install ghunna
// full package: includes the Tanzil Uthmani text (about 730 KB in the ESM build)
import { annotateVerse, getVerseText } from "ghunna";

// corpus-free core (about 34 KB): annotate your own text
import { annotate, tokenize, RULE_META } from "ghunna/core";

Both builds are shipped as ESM and CJS with full type declarations. There are no runtime dependencies.

Annotating a verse

const result = annotateVerse(36, 52);
// result.text         the verse text (Tanzil Uthmani); ranges index into it by codepoint
// result.annotations  Annotation[]
// result.riwayah      "hafs-shatibiyyah"

Each annotation has the shape:

{
  rule: RuleId;                  // e.g. "ikhfa-haqiqi"; 41 identifiers, see the catalog
  name: { arabic; transliteration; english };
  range: [start, end];           // codepoint indices into text, end exclusive
  trigger: { letters; description };
  derivation: string;            // filled template, English; derivationAr in RULE_META
  citation: { text: "tuhfah" | "jazariyyah"; lines: number[] };
  waqfDependent: boolean;
  appliesIn?: "wasl" | "waqf";   // present in mode "both"
  confidence: "certain" | "flagged";
}

Ranges are codepoint indices, not UTF-16 units. Iterate the text with [...text] or an equivalent codepoint-aware method before slicing.

What a whole verse looks like

The result is one string plus a flat list of spans, not letter-by-letter output. For surah 36 (Yā-Sīn), verse 52:

قَالُوا۟ يَٰوَيْلَنَا مَنۢ بَعَثَنَا مِن مَّرْقَدِنَا هَٰذَا مَا وَعَدَ ٱلرَّحْمَٰنُ وَصَدَقَ ٱلْمُرْسَلُونَ

The verse is 108 codepoints; the call returns 26 annotations. A selection, with the text each range slices out:

rulerangespan text
tafkhim-istila[0, 2]قَ
silent[6, 8]ا۟
iqlab[24, 29]نۢ بَ
idgham-bighunnah[39, 44]ن مَّ
hamzat-wasl[72, 73]ٱ
lam-shamsiyyah[73, 77]لرَّ
ra-takrir[74, 77]رَّ
ra-tafkhim[74, 77]رَّ
madd-tabii[81, 82]ٰ

Three observations. A span covers the letter together with its diacritics, and where a rule involves a following letter (iqlāb, idghām) the span covers both. Spans overlap freely: the doubled rāʾ of ٱلرَّحْمَٰنُ sits inside three annotations at once, the article's assimilation, the heavy rāʾ, and the concealed trill. And letters with no annotation are simply clear; the engine emits spans only where a rule applies.

The per-letter view

To ask what applies to one letter, filter the spans by position. annotationsAt does exactly that:

import { annotateVerse, annotationsAt } from "ghunna";

const { text, annotations } = annotateVerse(36, 52);
const cps = [...text];               // codepoints, not UTF-16 units

annotationsAt(annotations, 74);       // the ra of al-Rahman:
// [lam-shamsiyyah, ra-takrir, ra-tafkhim]

// a full letter-by-letter breakdown:
cps.map((ch, i) => ({ ch, rules: annotationsAt(annotations, i).map((a) => a.rule) }));

Slice ranges with [...text].slice(start, end), never text.slice(): the ranges are codepoint indices, and UTF-16 slicing corrupts any text containing characters outside the basic plane.

Recitation context

Several rules exist only at a pause, and several die across one. The engine therefore takes the recitation context as options rather than assuming it:

annotateVerse(1, 7,   { mode: "stop" });     // waqf at verse end
annotateVerse(2, 255, { mode: "both" });     // union; per-mode spans carry appliesIn
annotateVerse(36, 52, { stopAt: 5 });        // stop after word 5, resume at word 6
annotateVerse(1, 2,   { startFresh: true }); // utterance start: hamzat al-wasl vowel rules

The default mode is continuation into the following text. The semantics of each mode are specified in the waqf model.

The corpus-free core

import { annotate } from "ghunna/core";

annotate("مِن رَّبِّهِمْ");
// [{ rule: "idgham-bila-ghunnah", range: [...], ... }]

annotate(text, options) accepts the same mode options plus saktAfterWord, the transmitted sakt positions that annotateVerse applies automatically from the riwāyah data.

Input contract

  • Input is vocalized Uthmani Arabic in the Tanzil Uthmani encoding. The bundled corpus is exactly that text, carried intact.
  • Combining-mark order is preserved. Do not NFC-normalize the input; normalization reorders marks and destroys distinctions the tokenizer depends on.
  • Unknown codepoints, marks without a base letter, and empty input throw TokenizeError. Failure is loud by design: this is the Qurʾān, and the engine does not guess silently.
  • Where the sources transmit two admissible readings of a site, the annotation is emitted with confidence: "flagged" rather than silently choosing.

Beyond annotation

The package also exports the building blocks: tokenize (the letter model: letters, words, seats, vowels), RULE_META (the full rule table with names, citations and both derivation templates), describeLetter and LETTER_PROFILES (articulation point and attributes of each letter, cited to al-Jazariyyah's lines), getWaqfMarks (the printed stop marks of the Ḥafṣ muṣḥaf as data), and HAFS_SHATIBIYYAH (the riwāyah parameter set, including the four transmitted sakt sites). The complete surface is in the API reference.