@quran.ws/tajwid
v0.1.0
Published
Compiles the tajweed rule corpus and reports where each rule applies in Quranic text, as code-point offsets.
Maintainers
Readme
@quran.ws/tajwid
Compiles the tajweed rule corpus and reports where each rule applies in Quranic text.
npm install @quran.ws/tajwid @quran.ws/tajwid-rulesimport corpus from '@quran.ws/tajwid-rules'
import { Tajweed, sliceSpan } from '@quran.ws/tajwid'
const tajweed = new Tajweed(corpus)
for (const span of tajweed.analyze(ayahText)) {
console.log(span.hukumId, sliceSpan(ayahText, span))
}It returns positions, not markup
interface Span {
start: number // code-point position in the text YOU passed in
end: number // half-open
ruleId: string // 'madd-muttasil.1'
hukumId: string // 'madd-muttasil'
categoryId: string // 'madd-far-hamz'
topicId: string // 'madd'
}For example, analysing ayah 112:1 (قُلۡ هُوَ ٱللَّهُ أَحَدٌ) returns four spans, one of which is:
{ start: 22, end: 24, ruleId: 'qalqalah-kubra.1', hukumId: 'qalqalah-kubra', ... }
// sliceSpan(text, span) === 'دٌ' — the final د of أَحَدٌ, with its tanweenColours, HTML, ANSI, SVG overlays and mushaf-page coordinates all build on top of this, and each platform needs a different one. Returning positions keeps the engine useful for all of them; returning HTML would serve only one.
start and end count code points, not bytes and not UTF-16 units, so the
same numbers mean the same thing in PHP, Python and Swift. All Arabic and all
Quranic annotation marks sit in the Basic Multilingual Plane, so for Quranic
text the positions also work directly as JavaScript string indices —
sliceSpan handles the general case anyway.
Cutting the text is where Arabic breaks
Colouring a span means giving it its own element, and a browser shapes each element on its own — so the word comes apart at every change of colour, and a cut taken at the raw offset can land between a letter and the shadda written on it. The text is unaltered either way, nothing is raised, and it reads as a font problem. Two exports carry the fix:
import { bridgeJoins, clusterEnd } from '@quran.ws/tajwid'
const end = clusterEnd(text, span.end) // past the marks on that letter
const chunks = bridgeJoins([before, span, after]) // joiners across each cuttoHtml and @quran.ws/tajwid-react already use both. Write your
own renderer — an SVG overlay, a canvas, a native view — and you need them:
docs/rendering.md says why, including why the joiner
has to be conditional.
Spans overlap, on purpose
One letter can demonstrate more than one ruling, and analyze reports all of
them. Choosing what to draw is a display decision, not an engine decision:
import { resolveOverlaps } from '@quran.ws/tajwid'
resolveOverlaps(spans) // earliest wins, longest wins on a tieAn app that teaches tajweed probably wants to keep the overlaps rather than flatten them to one layer.
Choosing rules
new Tajweed(corpus, { only: ['madd'] }) // a topic
new Tajweed(corpus, { only: ['madd-muttasil'] }) // a hukum
new Tajweed(corpus, { only: ['madd-muttasil.1'] }) // one rule
new Tajweed(corpus, { school: 'ibn-al-jazari' }) // pick a school
new Tajweed(corpus, { includeDisabled: true }) // for working ON the corpusschool matters where the corpus models two authorities side by side — Ibn
al-Jazarī counts five ranks of tafkheem, Ibn al-Ṭaḥḥān counts three, and both
are present. Ahkam with no school attribution are always kept.
Your text is never changed
Matching runs on an internal normalised copy. Nothing in the pipeline applies
Unicode normalisation, strips diacritics, or drops waqf marks, and analyze
never returns a modified copy of your string — only positions into it.
Run it at build time
The Quran is a fixed text, so for production the expected setup is: annotate every ayah once and ship the positions, instead of compiling every pattern on every request. Record which text edition you computed against — the same rule lands on different positions in different editions of the Uthmani script.
Licence
MIT. The rule corpus is a separate work under CC BY 4.0.
