Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
TextDirection
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
7.01
0.00% covered (danger)
0.00%
0 / 1
 getDirection
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
7.01
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace Wikimedia\Leximorph\Provider;
8
9use IntlChar;
10
11/**
12 * TextDirection
13 *
14 * Determines whether text is primarily left-to-right or right-to-left.
15 *
16 * @since     1.45
17 * @author    Doğu Abaris (abaris@null.net)
18 * @license   https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
19 */
20class TextDirection {
21
22    /**
23     * Array of Unicode BIDI control codepoints used for directional formatting.
24     *
25     * Note: LRM (0x200E) and RLM (0x200F) are not included as they are standalone marks.
26     *
27     * @var int[]
28     */
29    private const array BIDI_CONTROL_CODES = [
30        // LEFT-TO-RIGHT EMBEDDING (LRE)
31        0x202A,
32        // RIGHT-TO-LEFT EMBEDDING (RLE)
33        0x202B,
34        // POP DIRECTIONAL FORMATTING (PDF)
35        0x202C,
36        // LEFT-TO-RIGHT OVERRIDE (LRO)
37        0x202D,
38        // RIGHT-TO-LEFT OVERRIDE (RLO)
39        0x202E,
40        // LEFT-TO-RIGHT ISOLATE (LRI)
41        0x2066,
42        // RIGHT-TO-LEFT ISOLATE (RLI)
43        0x2067,
44        // FIRST-STRONG ISOLATE (FSI)
45        0x2068,
46        // POP DIRECTIONAL ISOLATE (PDI)
47        0x2069,
48    ];
49
50    /**
51     * Determines the first strong directional Unicode codepoint.
52     *
53     * Iterates over Unicode characters using IntlChar. Skips BIDI control and
54     * unallocated characters (using {@see IntlChar::isdefined}). Returns 'ltr' if a
55     * left-to-right char is found, 'rtl' for right-to-left (incl. Arabic), or
56     * null if none is found.
57     *
58     * @param string $text Text to test.
59     *
60     * @since 1.45
61     * @return ?string 'ltr', 'rtl', or null.
62     */
63    public function getDirection( string $text ): ?string {
64        $length = mb_strlen( $text, 'UTF-8' );
65        for ( $i = 0; $i < $length; $i++ ) {
66            $char = mb_substr( $text, $i, 1, 'UTF-8' );
67            $code = IntlChar::ord( $char );
68
69            // Skip BIDI control characters.
70            if ( in_array( $code, self::BIDI_CONTROL_CODES, true ) ) {
71                continue;
72            }
73
74            // Skip unallocated characters.
75            if ( !IntlChar::isdefined( $code ) ) {
76                continue;
77            }
78
79            $dir = IntlChar::charDirection( $code );
80            if ( $dir === IntlChar::CHAR_DIRECTION_LEFT_TO_RIGHT ) {
81                return 'ltr';
82            }
83            if (
84                $dir === IntlChar::CHAR_DIRECTION_RIGHT_TO_LEFT ||
85                $dir === IntlChar::CHAR_DIRECTION_RIGHT_TO_LEFT_ARABIC
86            ) {
87                return 'rtl';
88            }
89        }
90
91        return null;
92    }
93}