Skip to main content

Split Arabic letters respecting their shape

Unlike Latin characters Arabic is inherently cursive. A single letter can have up to four different shapes depending on where it sits in a word: Isolated, Initial, Medial, or Final. The problem is that if words are splitted every character is assumed to be isolated because it looses the context of the surrounding letters, which makes the text look broken.

This helper function solves that without the need of any extra work:

function splitArabicText(target, config = {}) {
let type = config.type || "chars,words,lines",
splitWords = type.includes("words"),
splitLines = type.includes("lines"),
userOnSplit = config.onSplit;
return new SplitText(target, {
...config,
type: "words" + (splitLines ? ",lines" : ""),
onSplit(self) {
let ZWJ = "\u200D",
nonJoinAfter = /[اأإآدذرزوؤءة]/,
diacritics = /[\u064B-\u065F\u0670]/;
self.chars.length = 0;
self.words.forEach((wordEl) => {
let segs = [],
chars = Array.from(wordEl.textContent),
i = 0;
while (i < chars.length) {
let c = chars[i];
if (c === "\u0644" && i + 1 < chars.length) {
let j = i + 1,
d = "";
while (j < chars.length && diacritics.test(chars[j])) d += chars[j++];
if (j < chars.length && /[\u0622\u0623\u0625\u0627]/.test(chars[j])) {
c += d + chars[j];
i = j;
}
}
diacritics.test(c) && segs.length ? (segs[segs.length - 1] += c) : segs.push(c);
i++;
}
wordEl.textContent = "";
segs.forEach((seg, si) => {
let connects = (s) => !nonJoinAfter.test(s.replace(diacritics, "").slice(-1)),
s = seg;
if (si && connects(segs[si - 1])) s = ZWJ + s;
if (si < segs.length - 1 && connects(seg)) s += ZWJ;
let el = document.createElement(config.tag || "div");
config.tag === "span" || (el.style.display = "inline-block");
config.charsClass && (el.className = config.charsClass);
el.textContent = s;
wordEl.appendChild(el);
self.chars.push(el);
});
});
console.log("chars", self.chars);
if (!splitWords) {
self.words.forEach((w) => w.replaceWith(...w.childNodes));
self.words.length = 0;
}
userOnSplit?.(self);
},
});
}

Usage

const split = splitArabicText("#my-target", {
type: "chars",
charsClass: "prox-char",
});

Simple Demo

loading...