Files
decauthy/vendor/tecnickcom/tc-lib-unicode/src/Substitution/Devanagari.php
T
2026-10-08 23:10:11 +00:00

209 lines
6.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
* Devanagari.php
*
* @since 2026-04-30
* @category Library
* @package Unicode
* @author Nicola Asuni <info@tecnick.com>
* @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 <info@tecnick.com>
* @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<int>
*/
private array $ordarr;
/**
* @param array<int, int> $ordarr Array of Unicode codepoints.
*/
public function __construct(array $ordarr)
{
$this->ordarr = array_values($ordarr);
$this->process();
}
/**
* Returns the transformed codepoint array.
*
* @return list<int>
*/
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<int> $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<int> 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);
}
}