generated from jric11/baseProject
269 lines
7.0 KiB
PHP
269 lines
7.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Arabic.php
|
|
*
|
|
* @since 2011-05-23
|
|
* @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\Bidi\Shaping;
|
|
|
|
use Com\Tecnick\Unicode\Data\Arabic as UniArabic;
|
|
|
|
/**
|
|
* Com\Tecnick\Unicode\Bidi\Shaping\Arabic
|
|
*
|
|
* Joining context of the Arabic shaping: the form of a character is decided by the
|
|
* Joining_Type of the nearest non-transparent characters around it, as defined by
|
|
* ArabicShaping.txt and section 9.2 of the Unicode standard.
|
|
*
|
|
* @since 2015-07-13
|
|
* @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
|
|
*
|
|
* @phpstan-type CharData array{
|
|
* 'char': int,
|
|
* 'i': int,
|
|
* 'level': int,
|
|
* 'otype': string,
|
|
* 'pdimatch': int,
|
|
* 'pos': int,
|
|
* 'type': string,
|
|
* 'x': int,
|
|
* }
|
|
*
|
|
* @phpstan-type SeqData array{
|
|
* 'e': int,
|
|
* 'edir': string,
|
|
* 'end': int,
|
|
* 'eos': string,
|
|
* 'length': int,
|
|
* 'maxlevel': int,
|
|
* 'sos': string,
|
|
* 'start': int,
|
|
* 'item': array<int, CharData>,
|
|
* }
|
|
*/
|
|
abstract class Arabic
|
|
{
|
|
/**
|
|
* Index of the isolated form in the substitution rows.
|
|
*/
|
|
protected const FORM_ISOLATED = 0;
|
|
|
|
/**
|
|
* Index of the final form in the substitution rows.
|
|
*/
|
|
protected const FORM_FINAL = 1;
|
|
|
|
/**
|
|
* Index of the initial form in the substitution rows.
|
|
*/
|
|
protected const FORM_INITIAL = 2;
|
|
|
|
/**
|
|
* Index of the medial form in the substitution rows.
|
|
*/
|
|
protected const FORM_MEDIAL = 3;
|
|
|
|
/**
|
|
* Sequence to process and return
|
|
*
|
|
* @var SeqData
|
|
*/
|
|
protected array $seq = [
|
|
'e' => 0,
|
|
'edir' => '',
|
|
'end' => 0,
|
|
'eos' => '',
|
|
'length' => 0,
|
|
'maxlevel' => 0,
|
|
'sos' => '',
|
|
'start' => 0,
|
|
'item' => [],
|
|
];
|
|
|
|
/**
|
|
* Array of processed chars
|
|
*
|
|
* @var array<int, CharData>
|
|
*/
|
|
protected array $newchardata = [];
|
|
|
|
/**
|
|
* Codepoints of the paragraph the sequence belongs to, in logical order.
|
|
* The joining context is read from the paragraph and not from the sequence because
|
|
* X9 removes the joining-relevant format characters (ZWJ and ZWNJ) from the latter.
|
|
*
|
|
* @var array<int, int>
|
|
*/
|
|
protected array $paragraph = [];
|
|
|
|
/**
|
|
* Joining_Type of each paragraph position
|
|
*
|
|
* @var array<int, string>
|
|
*/
|
|
protected array $joining = [];
|
|
|
|
/**
|
|
* Index of the sequence item of each paragraph position
|
|
*
|
|
* @var array<int, int>
|
|
*/
|
|
protected array $seqindex = [];
|
|
|
|
/**
|
|
* Build the joining type of every paragraph position and the map from a paragraph
|
|
* position to the sequence item that holds it.
|
|
*/
|
|
protected function setJoiningContext(): void
|
|
{
|
|
$this->joining = [];
|
|
foreach ($this->paragraph as $pos => $ord) {
|
|
$this->joining[$pos] = UniArabic::getJoiningType($ord);
|
|
}
|
|
|
|
$this->seqindex = [];
|
|
foreach ($this->seq['item'] as $idx => $item) {
|
|
$this->seqindex[$item['pos']] = $idx;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Position of the nearest non-transparent character before the given one,
|
|
* or null at the beginning of the paragraph.
|
|
*/
|
|
protected function getPrevPosition(int $pos): ?int
|
|
{
|
|
for ($idx = $pos - 1; $idx >= 0; --$idx) {
|
|
if (($this->joining[$idx] ?? 'U') !== 'T') {
|
|
return $idx;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Position of the nearest non-transparent character after the given one,
|
|
* or null at the end of the paragraph.
|
|
*/
|
|
protected function getNextPosition(int $pos): ?int
|
|
{
|
|
$length = \count($this->paragraph);
|
|
for ($idx = $pos + 1; $idx < $length; ++$idx) {
|
|
if (($this->joining[$idx] ?? 'U') !== 'T') {
|
|
return $idx;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the Joining_Type of a paragraph position (Non_Joining outside the paragraph).
|
|
*/
|
|
protected function getJoiningType(?int $pos): string
|
|
{
|
|
if ($pos === null) {
|
|
return 'U';
|
|
}
|
|
|
|
return $this->joining[$pos] ?? 'U';
|
|
}
|
|
|
|
/**
|
|
* True when a character of the given joining type connects to the character that
|
|
* follows it: Dual_Joining, Join_Causing and Left_Joining do.
|
|
*/
|
|
protected function linksForward(string $type): bool
|
|
{
|
|
return $type === 'D' || $type === 'C' || $type === 'L';
|
|
}
|
|
|
|
/**
|
|
* True when a character of the given joining type connects to the character that
|
|
* precedes it: Dual_Joining, Join_Causing and Right_Joining do.
|
|
*/
|
|
protected function linksBackward(string $type): bool
|
|
{
|
|
return $type === 'D' || $type === 'C' || $type === 'R';
|
|
}
|
|
|
|
/**
|
|
* True when the character at the given paragraph position connects to the preceding one.
|
|
*/
|
|
protected function joinsPrev(int $pos): bool
|
|
{
|
|
return (
|
|
$this->linksBackward($this->getJoiningType($pos))
|
|
&& $this->linksForward($this->getJoiningType($this->getPrevPosition($pos)))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* True when the character at the given paragraph position connects to the following one.
|
|
*/
|
|
protected function joinsNext(int $pos): bool
|
|
{
|
|
return (
|
|
$this->linksForward($this->getJoiningType($pos))
|
|
&& $this->linksBackward($this->getJoiningType($this->getNextPosition($pos)))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Index of the presentation form of the character at the given paragraph position.
|
|
*/
|
|
protected function getForm(int $pos): int
|
|
{
|
|
$prev = $this->joinsPrev($pos);
|
|
$next = $this->joinsNext($pos);
|
|
|
|
return match (true) {
|
|
$prev && $next => self::FORM_MEDIAL,
|
|
$prev => self::FORM_FINAL,
|
|
$next => self::FORM_INITIAL,
|
|
default => self::FORM_ISOLATED,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Replace the character of a sequence item, or mark it for deletion with -1.
|
|
*/
|
|
protected function setNewChar(int $idx, int $char): void
|
|
{
|
|
$item = $this->newchardata[$idx] ?? null;
|
|
assert($item !== null, 'Expected shaped character at the requested index');
|
|
$item['char'] = $char;
|
|
$this->newchardata[$idx] = $item;
|
|
}
|
|
|
|
/**
|
|
* Returns the codepoint at the given paragraph position, or null outside of it.
|
|
*/
|
|
protected function getParagraphChar(?int $pos): ?int
|
|
{
|
|
if ($pos === null) {
|
|
return null;
|
|
}
|
|
|
|
return $this->paragraph[$pos] ?? null;
|
|
}
|
|
}
|