* @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 Test\Bidi; use Com\Tecnick\Unicode\Bidi\StepX; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; /** * Bidi Test * * @since 2011-05-23 * @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 */ class StepXTest extends TestCase { /** * @param array $ordarr Array of character codes */ #[DataProvider('stepXDataProvider')] public function testStepX(array $ordarr, int $pel, mixed $expected): void { $stepx = new StepX($ordarr, $pel); $this->assertEquals($expected, $stepx->getChrData()); } /** * X1 allows the embedding levels 0 through max_depth (125), so the level produced by * 125 nested isolate initiators is still valid and the 126th one overflows. */ public function testMaxDepth(): void { $ordarr = []; for ($idx = 0; $idx < StepX::MAX_DEPTH; ++$idx) { // Alternating RLI and LRI: each one adds exactly one level. $ordarr[] = ($idx % 2) === 0 ? 0x2067 : 0x2066; } $ordarr[] = 0x05D0; $stepx = new StepX($ordarr, 0); $chardata = $stepx->getChrData(); $last = \end($chardata); $this->assertIsArray($last); $this->assertSame(StepX::MAX_DEPTH, $last['level']); // One initiator more overflows: the letter stays at the last valid level. \array_splice($ordarr, StepX::MAX_DEPTH, 0, [0x2066]); $stepx = new StepX($ordarr, 0); $chardata = $stepx->getChrData(); $last = \end($chardata); $this->assertIsArray($last); $this->assertSame(StepX::MAX_DEPTH, $last['level']); } /** * @return array, * 1: int, * 2: array, * }> */ public static function stepXDataProvider(): array { return [ [ // BD13 Example 1: text1·RLE·text2·PDF·RLE·text3·PDF·text4 [33, 8235, 34, 8236, 8235, 38, 8236, 39], 0, [ [ 'x' => -1, 'pos' => 0, 'char' => 33, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 2, 'char' => 34, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 5, 'char' => 38, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 7, 'char' => 39, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], ], ], [ // BD13 Example 2: text1·RLI·text2·PDI·RLI·text3·PDI·text4 [33, 8295, 34, 8297, 8295, 38, 8297, 39], 0, [ [ 'x' => -1, 'pos' => 0, 'char' => 33, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 1, 'char' => 8295, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'RLI', 'otype' => 'RLI', ], [ 'x' => -1, 'pos' => 2, 'char' => 34, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 3, 'char' => 8297, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'PDI', 'otype' => 'PDI', ], [ 'x' => -1, 'pos' => 4, 'char' => 8295, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'RLI', 'otype' => 'RLI', ], [ 'x' => -1, 'pos' => 5, 'char' => 38, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 6, 'char' => 8297, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'PDI', 'otype' => 'PDI', ], [ 'x' => -1, 'pos' => 7, 'char' => 39, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], ], ], [ // BD13 Example 3: text1·RLI·text2·LRI·text3·RLE·text4·PDF·text5·PDI·text6·PDI·text7 [33, 8295, 34, 8294, 38, 8235, 39, 8236, 40, 8297, 41, 8297, 42], 0, [ [ 'x' => -1, 'pos' => 0, 'char' => 33, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 1, 'char' => 8295, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'RLI', 'otype' => 'RLI', ], [ 'x' => -1, 'pos' => 2, 'char' => 34, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 3, 'char' => 8294, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'LRI', 'otype' => 'LRI', ], [ 'x' => -1, 'pos' => 4, 'char' => 38, 'i' => -1, 'level' => 2, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 6, 'char' => 39, 'i' => -1, 'level' => 3, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 8, 'char' => 40, 'i' => -1, 'level' => 2, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 9, 'char' => 8297, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'PDI', 'otype' => 'PDI', ], [ 'x' => -1, 'pos' => 10, 'char' => 41, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 11, 'char' => 8297, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'PDI', 'otype' => 'PDI', ], [ 'x' => -1, 'pos' => 12, 'char' => 42, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], ], ], [ // X10 Example 1: text1·RLE·text2·LRE·text3·PDF·text4·PDF·RLE·text5·PDF·text6 [33, 8235, 34, 8234, 38, 8236, 39, 8236, 8235, 40, 8236, 41], 0, [ [ 'x' => -1, 'pos' => 0, 'char' => 33, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 2, 'char' => 34, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 4, 'char' => 38, 'i' => -1, 'level' => 2, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 6, 'char' => 39, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 9, 'char' => 40, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 11, 'char' => 41, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], ], ], [ // X10 Example 2: text1·RLI·text2·LRI·text3·PDI·text4·PDI·RLI·text5·PDI·text6 [33, 8295, 34, 8294, 38, 8297, 39, 8297, 8295, 40, 8297, 41], 0, [ [ 'x' => -1, 'pos' => 0, 'char' => 33, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 1, 'char' => 8295, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'RLI', 'otype' => 'RLI', ], [ 'x' => -1, 'pos' => 2, 'char' => 34, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 3, 'char' => 8294, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'LRI', 'otype' => 'LRI', ], [ 'x' => -1, 'pos' => 4, 'char' => 38, 'i' => -1, 'level' => 2, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 5, 'char' => 8297, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'PDI', 'otype' => 'PDI', ], [ 'x' => -1, 'pos' => 6, 'char' => 39, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 7, 'char' => 8297, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'PDI', 'otype' => 'PDI', ], [ 'x' => -1, 'pos' => 8, 'char' => 8295, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'RLI', 'otype' => 'RLI', ], [ 'x' => -1, 'pos' => 9, 'char' => 40, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 10, 'char' => 8297, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'PDI', 'otype' => 'PDI', ], [ 'x' => -1, 'pos' => 11, 'char' => 41, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], ], ], [ // X10 Example 3: text1·RLE·text2·LRI·text3·RLE·text4·PDI·text5·PDF·text6 [33, 8235, 34, 8294, 38, 8235, 39, 8297, 40, 8236, 41], 0, [ [ 'x' => -1, 'pos' => 0, 'char' => 33, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 2, 'char' => 34, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 3, 'char' => 8294, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'LRI', 'otype' => 'LRI', ], [ 'x' => -1, 'pos' => 4, 'char' => 38, 'i' => -1, 'level' => 2, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 6, 'char' => 39, 'i' => -1, 'level' => 3, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 7, 'char' => 8297, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'PDI', 'otype' => 'PDI', ], [ 'x' => -1, 'pos' => 8, 'char' => 40, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 10, 'char' => 41, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], ], ], [ // text1·RLO·text2·LRO·text3·RLO·text4·PDF·text5·PDF·text6 [33, 8238, 34, 8237, 38, 8238, 39, 8236, 40, 8236, 41], 0, [ [ 'x' => -1, 'pos' => 0, 'char' => 33, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 2, 'char' => 34, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'R', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 4, 'char' => 38, 'i' => -1, 'level' => 2, 'pdimatch' => -1, 'type' => 'L', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 6, 'char' => 39, 'i' => -1, 'level' => 3, 'pdimatch' => -1, 'type' => 'R', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 8, 'char' => 40, 'i' => -1, 'level' => 2, 'pdimatch' => -1, 'type' => 'L', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 10, 'char' => 41, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'R', 'otype' => 'ON', ], ], ], [ // text1·FSI·text2·PDI·text3 [33, 8296, 34, 8297, 38], 0, [ [ 'x' => -1, 'pos' => 0, 'char' => 33, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 1, 'char' => 8296, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'FSI', 'otype' => 'FSI', ], [ 'x' => -1, 'pos' => 2, 'char' => 34, 'i' => -1, 'level' => 2, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 3, 'char' => 8297, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'PDI', 'otype' => 'PDI', ], [ 'x' => -1, 'pos' => 4, 'char' => 38, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], ], ], [ // text1·FSI·text2·PDI·text3 [1488, 8296, 1489, 8297, 1490], 1, [ [ 'x' => -1, 'pos' => 0, 'char' => 1488, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'R', 'otype' => 'R', ], [ 'x' => -1, 'pos' => 1, 'char' => 8296, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'FSI', 'otype' => 'FSI', ], [ 'x' => -1, 'pos' => 2, 'char' => 1489, 'i' => -1, 'level' => 3, 'pdimatch' => -1, 'type' => 'R', 'otype' => 'R', ], [ 'x' => -1, 'pos' => 3, 'char' => 8297, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'PDI', 'otype' => 'PDI', ], [ 'x' => -1, 'pos' => 4, 'char' => 1490, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'R', 'otype' => 'R', ], ], ], [ // text1·BN·text2·BN·text3 (U+200B ZERO WIDTH SPACE) [33, 8203, 34, 8203, 38], 0, [ [ 'x' => -1, 'pos' => 0, 'char' => 33, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 2, 'char' => 34, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 4, 'char' => 38, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], ], ], [ // Test overflow: text1·120xLRE·LRI·PDF·PDI·PDF·PDI·text2 // X6a keeps both PDIs: the first matches an overflow isolate, the second matches none. [ 33, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8234, 8294, 8236, 8297, 8236, 8297, 34, ], 0, [ [ 'x' => -1, 'pos' => 0, 'char' => 33, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 121, 'char' => 8294, 'i' => -1, 'level' => 124, 'pdimatch' => -1, 'type' => 'LRI', 'otype' => 'LRI', ], [ 'x' => -1, 'pos' => 123, 'char' => 8297, 'i' => -1, 'level' => 124, 'pdimatch' => -1, 'type' => 'PDI', 'otype' => 'PDI', ], [ 'x' => -1, 'pos' => 125, 'char' => 8297, 'i' => -1, 'level' => 124, 'pdimatch' => -1, 'type' => 'PDI', 'otype' => 'PDI', ], [ 'x' => -1, 'pos' => 126, 'char' => 34, 'i' => -1, 'level' => 124, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], ], ], [ // RLO + text + PDF: text type is overridden to R (covers X4 / RLO case) [33, 8238, 34, 8236, 39], 0, [ [ 'x' => -1, 'pos' => 0, 'char' => 33, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 2, 'char' => 34, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'R', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 4, 'char' => 39, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], ], ], [ // RTL + LRO + text + PDF + RTL: text type is overridden to L (covers X5 / LRO case) [1488, 8237, 34, 8236, 1489], 1, [ [ 'x' => -1, 'pos' => 0, 'char' => 1488, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'R', 'otype' => 'R', ], [ 'x' => -1, 'pos' => 2, 'char' => 34, 'i' => -1, 'level' => 2, 'pdimatch' => -1, 'type' => 'L', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 4, 'char' => 1489, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'R', 'otype' => 'R', ], ], ], [ // text + FSI + Hebrew + Hebrew + PDI + text: FSI detects RTL (covers X5c / FSI case) [33, 8296, 1488, 1489, 8297, 39], 0, [ [ 'x' => -1, 'pos' => 0, 'char' => 33, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], [ 'x' => -1, 'pos' => 1, 'char' => 8296, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'FSI', 'otype' => 'FSI', ], [ 'x' => -1, 'pos' => 2, 'char' => 1488, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'R', 'otype' => 'R', ], [ 'x' => -1, 'pos' => 3, 'char' => 1489, 'i' => -1, 'level' => 1, 'pdimatch' => -1, 'type' => 'R', 'otype' => 'R', ], [ 'x' => -1, 'pos' => 4, 'char' => 8297, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'PDI', 'otype' => 'PDI', ], [ 'x' => -1, 'pos' => 5, 'char' => 39, 'i' => -1, 'level' => 0, 'pdimatch' => -1, 'type' => 'ON', 'otype' => 'ON', ], ], ], ]; } }