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:
| rule | range | span 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.