* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD * @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE) * @link https://github.com/tecnickcom/tc-lib-unicode * * This file is part of tc-lib-unicode software library. */ namespace Com\Tecnick\Unicode\Substitution; use Com\Tecnick\Unicode\Data\Devanagari as DevanagariData; /** * Com\Tecnick\Unicode\Substitution\Devanagari * * Repositions Devanagari left-positional matras (vowel signs with Indic * Positional Category "Left", such as U+093F VOWEL SIGN I) to precede their * base consonant cluster in the codepoint array. * * In Unicode logical order a left matra is stored after the consonant (or * conjunct cluster) it modifies. For PDF glyph streams the matra glyph must * appear before the consonant, so each left matra is moved to immediately * before the cluster that precedes it. * * A consonant cluster is: base_consonant (VIRAMA base_consonant)* * Only base consonants in the range U+0915–U+0939 and U+0958–U+095F are * recognised as cluster heads. Orphaned matras (no preceding consonant) and * unknown codepoints are left unchanged. * * @since 2026-04-30 * @category Library * @package Unicode * @author Nicola Asuni * @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD * @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE) * @link https://github.com/tecnickcom/tc-lib-unicode */ final class Devanagari { /** * Transformed codepoint array. * * @var list */ private array $ordarr; /** * @param array $ordarr Array of Unicode codepoints. */ public function __construct(array $ordarr) { $this->ordarr = array_values($ordarr); $this->process(); } /** * Returns the transformed codepoint array. * * @return list */ public function getOrdarr(): array { return $this->ordarr; } /** * Iterates over the codepoint array and repositions left matras that * follow a consonant cluster. */ private function process(): void { $len = count($this->ordarr); $result = []; $idx = 0; while ($idx < $len) { $cp = $this->ordarr[$idx] ?? null; if ($cp === null) { ++$idx; continue; } if ($this->isBaseConsonant($cp)) { $idx = $this->processConsonantCluster($idx, $len, $result); continue; } $result[] = $cp; ++$idx; } $this->ordarr = array_values($result); } /** * Collects the full conjunct cluster starting at $idx, then checks * whether the next codepoint is a left matra. If so, emits the matra * first and the cluster second; otherwise emits the cluster as-is. * * @param int $idx Current index in $this->ordarr. * @param int $len Length of $this->ordarr. * @param list $result Result accumulator (passed by reference). * * @return int Updated index after all consumed codepoints. */ private function processConsonantCluster(int $idx, int $len, array &$result): int { $cluster = $this->collectCluster($idx, $len); $endIdx = $idx + count($cluster); if ($endIdx < $len) { $matra = $this->ordarr[$endIdx] ?? null; if ($matra !== null && $this->isLeftMatra($matra)) { $result[] = $matra; foreach ($cluster as $codepoint) { $result[] = $codepoint; } return $endIdx + 1; } } foreach ($cluster as $codepoint) { $result[] = $codepoint; } return $endIdx; } /** * Collects the full conjunct cluster: base_consonant (VIRAMA base_consonant)* * * @param int $idx Starting index of the cluster head. * @param int $len Length of $this->ordarr. * * @return list Collected cluster codepoints. */ private function collectCluster(int $idx, int $len): array { $first = $this->ordarr[$idx] ?? null; if ($first === null) { return []; } $cluster = [$first]; $pos = $idx + 1; while (($pos + 1) < $len) { $virama = $this->ordarr[$pos] ?? null; $baseConsonant = $this->ordarr[$pos + 1] ?? null; assert($baseConsonant !== null, 'Expected Devanagari base consonant after virama candidate'); if ($virama !== DevanagariData::VIRAMA || !$this->isBaseConsonant($baseConsonant)) { break; } $cluster[] = $virama; $cluster[] = $baseConsonant; $pos += 2; } return $cluster; } /** * Returns true when $codepoint is a Devanagari base consonant. */ private function isBaseConsonant(int $codepoint): bool { return $this->isInStandardRange($codepoint) || $this->isInExtendedRange($codepoint); } /** * Returns true when $codepoint is in the standard consonant range * U+0915–U+0939. */ private function isInStandardRange(int $codepoint): bool { return $codepoint >= DevanagariData::BASE_CONSONANT_FIRST && $codepoint <= DevanagariData::BASE_CONSONANT_LAST; } /** * Returns true when $codepoint is in the extended consonant range * U+0958–U+095F. */ private function isInExtendedRange(int $codepoint): bool { return ( $codepoint >= DevanagariData::BASE_CONSONANT_EXT_FIRST && $codepoint <= DevanagariData::BASE_CONSONANT_EXT_LAST ); } /** * Returns true when $codepoint is a Devanagari left matra. */ private function isLeftMatra(int $codepoint): bool { return array_key_exists($codepoint, DevanagariData::LEFT_MATRAS); } }