xref: /webtrees/app/Report/RightToLeftSupport.php (revision bd29d468ac3a5ed7a23d7dd8f300504dda83fc72)
1d5e02c3aSGreg Roach<?php
2d5e02c3aSGreg Roach
3d5e02c3aSGreg Roach/**
4d5e02c3aSGreg Roach * webtrees: online genealogy
55bfc6897SGreg Roach * Copyright (C) 2022 webtrees development team
6d5e02c3aSGreg Roach * This program is free software: you can redistribute it and/or modify
7d5e02c3aSGreg Roach * it under the terms of the GNU General Public License as published by
8d5e02c3aSGreg Roach * the Free Software Foundation, either version 3 of the License, or
9d5e02c3aSGreg Roach * (at your option) any later version.
10d5e02c3aSGreg Roach * This program is distributed in the hope that it will be useful,
11d5e02c3aSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12d5e02c3aSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13d5e02c3aSGreg Roach * GNU General Public License for more details.
14d5e02c3aSGreg Roach * You should have received a copy of the GNU General Public License
15d5e02c3aSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16d5e02c3aSGreg Roach */
17d5e02c3aSGreg Roach
18d5e02c3aSGreg Roachdeclare(strict_types=1);
19d5e02c3aSGreg Roach
20d5e02c3aSGreg Roachnamespace Fisharebest\Webtrees\Report;
21d5e02c3aSGreg Roach
22d5e02c3aSGreg Roachuse Fisharebest\Webtrees\I18N;
23d5e02c3aSGreg Roach
24005852d3SGreg Roachuse function ord;
25005852d3SGreg Roachuse function preg_replace;
26d5e02c3aSGreg Roachuse function str_contains;
27005852d3SGreg Roachuse function str_pad;
28005852d3SGreg Roachuse function str_replace;
29005852d3SGreg Roachuse function strlen;
30005852d3SGreg Roachuse function strpos;
31005852d3SGreg Roachuse function strrpos;
32005852d3SGreg Roachuse function strtolower;
33005852d3SGreg Roachuse function strtoupper;
34005852d3SGreg Roachuse function substr;
35005852d3SGreg Roach
36005852d3SGreg Roachuse const STR_PAD_LEFT;
37005852d3SGreg Roachuse const STR_PAD_RIGHT;
38d5e02c3aSGreg Roach
39d5e02c3aSGreg Roach/**
40d5e02c3aSGreg Roach * RTL Functions for use in the PDF reports
41d5e02c3aSGreg Roach */
42d5e02c3aSGreg Roachclass RightToLeftSupport
43d5e02c3aSGreg Roach{
44d5e02c3aSGreg Roach    private const UTF8_LRM = "\xE2\x80\x8E"; // U+200E (Left to Right mark:  zero-width character with LTR directionality)
45d5e02c3aSGreg Roach    private const UTF8_RLM = "\xE2\x80\x8F"; // U+200F (Right to Left mark:  zero-width character with RTL directionality)
46d5e02c3aSGreg Roach    private const UTF8_LRO = "\xE2\x80\xAD"; // U+202D (Left to Right override: force everything following to LTR mode)
47d5e02c3aSGreg Roach    private const UTF8_RLO = "\xE2\x80\xAE"; // U+202E (Right to Left override: force everything following to RTL mode)
48d5e02c3aSGreg Roach    private const UTF8_LRE = "\xE2\x80\xAA"; // U+202A (Left to Right embedding: treat everything following as LTR text)
49d5e02c3aSGreg Roach    private const UTF8_RLE = "\xE2\x80\xAB"; // U+202B (Right to Left embedding: treat everything following as RTL text)
50d5e02c3aSGreg Roach    private const UTF8_PDF = "\xE2\x80\xAC"; // U+202C (Pop directional formatting: restore state prior to last LRO, RLO, LRE, RLE)
51d5e02c3aSGreg Roach
52d5e02c3aSGreg Roach    private const OPEN_PARENTHESES = '([{';
53d5e02c3aSGreg Roach
54d5e02c3aSGreg Roach    private const CLOSE_PARENTHESES = ')]}';
55d5e02c3aSGreg Roach
56d5e02c3aSGreg Roach    private const NUMBERS = '0123456789';
57d5e02c3aSGreg Roach
58d5e02c3aSGreg Roach    private const NUMBER_PREFIX = '+-'; // Treat these like numbers when at beginning or end of numeric strings
59d5e02c3aSGreg Roach
60d5e02c3aSGreg Roach    private const NUMBER_PUNCTUATION = '- ,.:/'; // Treat these like numbers when inside numeric strings
61d5e02c3aSGreg Roach
62d5e02c3aSGreg Roach    private const PUNCTUATION = ',.:;?!';
63d5e02c3aSGreg Roach
64d5e02c3aSGreg Roach    // Markup
65d5e02c3aSGreg Roach    private const START_LTR    = '<LTR>';
66d5e02c3aSGreg Roach    private const END_LTR      = '</LTR>';
67d5e02c3aSGreg Roach    private const START_RTL    = '<RTL>';
68d5e02c3aSGreg Roach    private const END_RTL      = '</RTL>';
69d5e02c3aSGreg Roach    private const LENGTH_START = 5;
70d5e02c3aSGreg Roach    private const LENGTH_END   = 6;
71d5e02c3aSGreg Roach
727fa97a69SGreg Roach    /* Were we previously processing LTR or RTL. */
737fa97a69SGreg Roach    private static string $previousState;
74d5e02c3aSGreg Roach
757fa97a69SGreg Roach    /* Are we currently processing LTR or RTL. */
767fa97a69SGreg Roach    private static string $currentState;
77d5e02c3aSGreg Roach
787fa97a69SGreg Roach    /* Text waiting to be processed. */
797fa97a69SGreg Roach    private static string $waitingText;
80d5e02c3aSGreg Roach
817fa97a69SGreg Roach    /* Offset into the text. */
827fa97a69SGreg Roach    private static int $posSpanStart;
83d5e02c3aSGreg Roach
84d5e02c3aSGreg Roach    /**
85d5e02c3aSGreg Roach     * This function strips &lrm; and &rlm; from the input string. It should be used for all
86d5e02c3aSGreg Roach     * text that has been passed through the PrintReady() function before that text is stored
87d5e02c3aSGreg Roach     * in the database. The database should NEVER contain these characters.
88d5e02c3aSGreg Roach     *
89d5e02c3aSGreg Roach     * @param string $inputText The string from which the &lrm; and &rlm; characters should be stripped
90d5e02c3aSGreg Roach     *
91d5e02c3aSGreg Roach     * @return string The input string, with &lrm; and &rlm; stripped
92d5e02c3aSGreg Roach     */
93d5e02c3aSGreg Roach    private static function stripLrmRlm(string $inputText): string
94d5e02c3aSGreg Roach    {
95d5e02c3aSGreg Roach        return str_replace([
96d5e02c3aSGreg Roach            self::UTF8_LRM,
97d5e02c3aSGreg Roach            self::UTF8_RLM,
98d5e02c3aSGreg Roach            self::UTF8_LRO,
99d5e02c3aSGreg Roach            self::UTF8_RLO,
100d5e02c3aSGreg Roach            self::UTF8_LRE,
101d5e02c3aSGreg Roach            self::UTF8_RLE,
102d5e02c3aSGreg Roach            self::UTF8_PDF,
103d5e02c3aSGreg Roach            '&lrm;',
104d5e02c3aSGreg Roach            '&rlm;',
105d5e02c3aSGreg Roach            '&LRM;',
106d5e02c3aSGreg Roach            '&RLM;',
107d5e02c3aSGreg Roach        ], '', $inputText);
108d5e02c3aSGreg Roach    }
109d5e02c3aSGreg Roach
110d5e02c3aSGreg Roach    /**
111d5e02c3aSGreg Roach     * This function encapsulates all texts in the input with <span dir='xxx'> and </span>
112d5e02c3aSGreg Roach     * according to the directionality specified.
113d5e02c3aSGreg Roach     *
114d5e02c3aSGreg Roach     * @param string $inputText Raw input
115d5e02c3aSGreg Roach     *
116d5e02c3aSGreg Roach     * @return string The string with all texts encapsulated as required
117d5e02c3aSGreg Roach     */
118d5e02c3aSGreg Roach    public static function spanLtrRtl(string $inputText): string
119d5e02c3aSGreg Roach    {
120d5e02c3aSGreg Roach        if ($inputText === '') {
121d5e02c3aSGreg Roach            // Nothing to do
122d5e02c3aSGreg Roach            return '';
123d5e02c3aSGreg Roach        }
124d5e02c3aSGreg Roach
125d5e02c3aSGreg Roach        $workingText = str_replace("\n", '<br>', $inputText);
126d5e02c3aSGreg Roach        $workingText = str_replace([
127d5e02c3aSGreg Roach            '<span class="starredname"><br>',
128d5e02c3aSGreg Roach            '<span<br>class="starredname">',
129d5e02c3aSGreg Roach        ], '<br><span class="starredname">', $workingText); // Reposition some incorrectly placed line breaks
130d5e02c3aSGreg Roach        $workingText = self::stripLrmRlm($workingText); // Get rid of any existing UTF8 control codes
131d5e02c3aSGreg Roach
132d5e02c3aSGreg Roach        self::$previousState = '';
133d5e02c3aSGreg Roach        self::$currentState  = strtoupper(I18N::direction());
134d5e02c3aSGreg Roach        $numberState         = false; // Set when we're inside a numeric string
135d5e02c3aSGreg Roach        $result              = '';
136d5e02c3aSGreg Roach        self::$waitingText   = '';
137d5e02c3aSGreg Roach        $openParDirection    = [];
138d5e02c3aSGreg Roach
139d5e02c3aSGreg Roach        self::beginCurrentSpan($result);
140d5e02c3aSGreg Roach
141d5e02c3aSGreg Roach        while ($workingText !== '') {
142d5e02c3aSGreg Roach            $charArray     = self::getChar($workingText, 0); // Get the next ASCII or UTF-8 character
143d5e02c3aSGreg Roach            $currentLetter = $charArray['letter'];
144d5e02c3aSGreg Roach            $currentLen    = $charArray['length'];
145d5e02c3aSGreg Roach
146d5e02c3aSGreg Roach            $openParIndex  = strpos(self::OPEN_PARENTHESES, $currentLetter); // Which opening parenthesis is this?
147d5e02c3aSGreg Roach            $closeParIndex = strpos(self::CLOSE_PARENTHESES, $currentLetter); // Which closing parenthesis is this?
148d5e02c3aSGreg Roach
149d5e02c3aSGreg Roach            switch ($currentLetter) {
150d5e02c3aSGreg Roach                case '<':
151d5e02c3aSGreg Roach                    // Assume this '<' starts an HTML element
152d5e02c3aSGreg Roach                    $endPos = strpos($workingText, '>'); // look for the terminating '>'
153d5e02c3aSGreg Roach                    if ($endPos === false) {
154d5e02c3aSGreg Roach                        $endPos = 0;
155d5e02c3aSGreg Roach                    }
156d5e02c3aSGreg Roach                    $currentLen += $endPos;
157d5e02c3aSGreg Roach                    $element    = substr($workingText, 0, $currentLen);
158d5e02c3aSGreg Roach                    $temp       = strtolower(substr($element, 0, 3));
159d5e02c3aSGreg Roach                    if (strlen($element) < 7 && $temp === '<br') {
160d5e02c3aSGreg Roach                        if ($numberState) {
161d5e02c3aSGreg Roach                            $numberState = false;
162d5e02c3aSGreg Roach                            if (self::$currentState === 'RTL') {
163d5e02c3aSGreg Roach                                self::$waitingText .= self::UTF8_PDF;
164d5e02c3aSGreg Roach                            }
165d5e02c3aSGreg Roach                        }
166d5e02c3aSGreg Roach                        self::breakCurrentSpan($result);
167d5e02c3aSGreg Roach                    } elseif (self::$waitingText === '') {
168d5e02c3aSGreg Roach                        $result .= $element;
169d5e02c3aSGreg Roach                    } else {
170d5e02c3aSGreg Roach                        self::$waitingText .= $element;
171d5e02c3aSGreg Roach                    }
172d5e02c3aSGreg Roach                    $workingText = substr($workingText, $currentLen);
173d5e02c3aSGreg Roach                    break;
174d5e02c3aSGreg Roach                case '&':
175d5e02c3aSGreg Roach                    // Assume this '&' starts an HTML entity
176d5e02c3aSGreg Roach                    $endPos = strpos($workingText, ';'); // look for the terminating ';'
177d5e02c3aSGreg Roach                    if ($endPos === false) {
178d5e02c3aSGreg Roach                        $endPos = 0;
179d5e02c3aSGreg Roach                    }
180d5e02c3aSGreg Roach                    $currentLen += $endPos;
181d5e02c3aSGreg Roach                    $entity     = substr($workingText, 0, $currentLen);
182d5e02c3aSGreg Roach                    if (strtolower($entity) === '&nbsp;') {
183d5e02c3aSGreg Roach                        $entity = '&nbsp;'; // Ensure consistent case for this entity
184d5e02c3aSGreg Roach                    }
185d5e02c3aSGreg Roach                    if (self::$waitingText === '') {
186d5e02c3aSGreg Roach                        $result .= $entity;
187d5e02c3aSGreg Roach                    } else {
188d5e02c3aSGreg Roach                        self::$waitingText .= $entity;
189d5e02c3aSGreg Roach                    }
190d5e02c3aSGreg Roach                    $workingText = substr($workingText, $currentLen);
191d5e02c3aSGreg Roach                    break;
192d5e02c3aSGreg Roach                case '{':
193d5e02c3aSGreg Roach                    if (substr($workingText, 1, 1) === '{') {
194d5e02c3aSGreg Roach                        // Assume this '{{' starts a TCPDF directive
195d5e02c3aSGreg Roach                        $endPos = strpos($workingText, '}}'); // look for the terminating '}}'
196d5e02c3aSGreg Roach                        if ($endPos === false) {
197d5e02c3aSGreg Roach                            $endPos = 0;
198d5e02c3aSGreg Roach                        }
199d5e02c3aSGreg Roach                        $currentLen        = $endPos + 2;
200d5e02c3aSGreg Roach                        $directive         = substr($workingText, 0, $currentLen);
201d5e02c3aSGreg Roach                        $workingText       = substr($workingText, $currentLen);
202d5e02c3aSGreg Roach                        $result            .= self::$waitingText . $directive;
203d5e02c3aSGreg Roach                        self::$waitingText = '';
204d5e02c3aSGreg Roach                        break;
205d5e02c3aSGreg Roach                    }
206d5e02c3aSGreg Roach                // no break
207d5e02c3aSGreg Roach                default:
208d5e02c3aSGreg Roach                    // Look for strings of numbers with optional leading or trailing + or -
209d5e02c3aSGreg Roach                    // and with optional embedded numeric punctuation
210d5e02c3aSGreg Roach                    if ($numberState) {
211d5e02c3aSGreg Roach                        // If we're inside a numeric string, look for reasons to end it
212d5e02c3aSGreg Roach                        $offset    = 0; // Be sure to look at the current character first
213d5e02c3aSGreg Roach                        $charArray = self::getChar($workingText . "\n", $offset);
214d5e02c3aSGreg Roach                        if (!str_contains(self::NUMBERS, $charArray['letter'])) {
215d5e02c3aSGreg Roach                            // This is not a digit. Is it numeric punctuation?
216d5e02c3aSGreg Roach                            if (substr($workingText . "\n", $offset, 6) === '&nbsp;') {
217d5e02c3aSGreg Roach                                $offset += 6; // This could be numeric punctuation
218d5e02c3aSGreg Roach                            } elseif (str_contains(self::NUMBER_PUNCTUATION, $charArray['letter'])) {
219d5e02c3aSGreg Roach                                $offset += $charArray['length']; // This could be numeric punctuation
220d5e02c3aSGreg Roach                            }
221d5e02c3aSGreg Roach                            // If the next character is a digit, the current character is numeric punctuation
222d5e02c3aSGreg Roach                            $charArray = self::getChar($workingText . "\n", $offset);
223d5e02c3aSGreg Roach                            if (!str_contains(self::NUMBERS, $charArray['letter'])) {
224d5e02c3aSGreg Roach                                // This is not a digit. End the run of digits and punctuation.
225d5e02c3aSGreg Roach                                $numberState = false;
226d5e02c3aSGreg Roach                                if (self::$currentState === 'RTL') {
227d5e02c3aSGreg Roach                                    if (!str_contains(self::NUMBER_PREFIX, $currentLetter)) {
228d5e02c3aSGreg Roach                                        $currentLetter = self::UTF8_PDF . $currentLetter;
229d5e02c3aSGreg Roach                                    } else {
230d5e02c3aSGreg Roach                                        $currentLetter .= self::UTF8_PDF; // Include a trailing + or - in the run
231d5e02c3aSGreg Roach                                    }
232d5e02c3aSGreg Roach                                }
233d5e02c3aSGreg Roach                            }
234d5e02c3aSGreg Roach                        }
2356dcdd572SGreg Roach                    } elseif (str_contains(self::NUMBER_PREFIX, $currentLetter)) {
236d5e02c3aSGreg Roach                        // If we're outside a numeric string, look for reasons to start it
237d5e02c3aSGreg Roach                        // This might be a number lead-in
238d5e02c3aSGreg Roach                        $offset   = $currentLen;
239d5e02c3aSGreg Roach                        $nextChar = substr($workingText . "\n", $offset, 1);
240d5e02c3aSGreg Roach                        if (str_contains(self::NUMBERS, $nextChar)) {
241d5e02c3aSGreg Roach                            $numberState = true; // We found a digit: the lead-in is therefore numeric
242d5e02c3aSGreg Roach                            if (self::$currentState === 'RTL') {
243d5e02c3aSGreg Roach                                $currentLetter = self::UTF8_LRE . $currentLetter;
244d5e02c3aSGreg Roach                            }
245d5e02c3aSGreg Roach                        }
246d5e02c3aSGreg Roach                    } elseif (str_contains(self::NUMBERS, $currentLetter)) {
247d5e02c3aSGreg Roach                        $numberState = true; // The current letter is a digit
248d5e02c3aSGreg Roach                        if (self::$currentState === 'RTL') {
249d5e02c3aSGreg Roach                            $currentLetter = self::UTF8_LRE . $currentLetter;
250d5e02c3aSGreg Roach                        }
251d5e02c3aSGreg Roach                    }
252d5e02c3aSGreg Roach
253d5e02c3aSGreg Roach                    // Determine the directionality of the current UTF-8 character
254d5e02c3aSGreg Roach                    $newState = self::$currentState;
255d5e02c3aSGreg Roach
256d5e02c3aSGreg Roach                    while (true) {
257d5e02c3aSGreg Roach                        if (I18N::scriptDirection(I18N::textScript($currentLetter)) === 'rtl') {
258d5e02c3aSGreg Roach                            if (self::$currentState === '') {
259d5e02c3aSGreg Roach                                $newState = 'RTL';
260d5e02c3aSGreg Roach                                break;
261d5e02c3aSGreg Roach                            }
262d5e02c3aSGreg Roach
263d5e02c3aSGreg Roach                            if (self::$currentState === 'RTL') {
264d5e02c3aSGreg Roach                                break;
265d5e02c3aSGreg Roach                            }
266d5e02c3aSGreg Roach                            // Switch to RTL only if this isn't a solitary RTL letter
267d5e02c3aSGreg Roach                            $tempText = substr($workingText, $currentLen);
268d5e02c3aSGreg Roach                            while ($tempText !== '') {
269d5e02c3aSGreg Roach                                $nextCharArray = self::getChar($tempText, 0);
270d5e02c3aSGreg Roach                                $nextLetter    = $nextCharArray['letter'];
271d5e02c3aSGreg Roach                                $nextLen       = $nextCharArray['length'];
272d5e02c3aSGreg Roach                                $tempText      = substr($tempText, $nextLen);
273d5e02c3aSGreg Roach
274d5e02c3aSGreg Roach                                if (I18N::scriptDirection(I18N::textScript($nextLetter)) === 'rtl') {
275d5e02c3aSGreg Roach                                    $newState = 'RTL';
276d5e02c3aSGreg Roach                                    break 2;
277d5e02c3aSGreg Roach                                }
278d5e02c3aSGreg Roach
279d5e02c3aSGreg Roach                                if (str_contains(self::PUNCTUATION, $nextLetter) || str_contains(self::OPEN_PARENTHESES, $nextLetter)) {
280d5e02c3aSGreg Roach                                    $newState = 'RTL';
281d5e02c3aSGreg Roach                                    break 2;
282d5e02c3aSGreg Roach                                }
283d5e02c3aSGreg Roach
284d5e02c3aSGreg Roach                                if ($nextLetter === ' ') {
285d5e02c3aSGreg Roach                                    break;
286d5e02c3aSGreg Roach                                }
287d5e02c3aSGreg Roach                                $nextLetter .= substr($tempText . "\n", 0, 5);
288d5e02c3aSGreg Roach                                if ($nextLetter === '&nbsp;') {
289d5e02c3aSGreg Roach                                    break;
290d5e02c3aSGreg Roach                                }
291d5e02c3aSGreg Roach                            }
292d5e02c3aSGreg Roach                            // This is a solitary RTL letter : wrap it in UTF8 control codes to force LTR directionality
293d5e02c3aSGreg Roach                            $currentLetter = self::UTF8_LRO . $currentLetter . self::UTF8_PDF;
294d5e02c3aSGreg Roach                            $newState      = 'LTR';
295d5e02c3aSGreg Roach                            break;
296d5e02c3aSGreg Roach                        }
297d5e02c3aSGreg Roach                        if ($currentLen !== 1 || $currentLetter >= 'A' && $currentLetter <= 'Z' || $currentLetter >= 'a' && $currentLetter <= 'z') {
298d5e02c3aSGreg Roach                            // Since it’s neither Hebrew nor Arabic, this UTF-8 character or ASCII letter must be LTR
299d5e02c3aSGreg Roach                            $newState = 'LTR';
300d5e02c3aSGreg Roach                            break;
301d5e02c3aSGreg Roach                        }
302d5e02c3aSGreg Roach                        if ($closeParIndex !== false) {
303d5e02c3aSGreg Roach                            // This closing parenthesis has to inherit the matching opening parenthesis' directionality
304d5e02c3aSGreg Roach                            if (!empty($openParDirection[$closeParIndex]) && $openParDirection[$closeParIndex] !== '?') {
305d5e02c3aSGreg Roach                                $newState = $openParDirection[$closeParIndex];
306d5e02c3aSGreg Roach                            }
307d5e02c3aSGreg Roach                            $openParDirection[$closeParIndex] = '';
308d5e02c3aSGreg Roach                            break;
309d5e02c3aSGreg Roach                        }
310d5e02c3aSGreg Roach                        if ($openParIndex !== false) {
311d5e02c3aSGreg Roach                            // Opening parentheses always inherit the following directionality
312d5e02c3aSGreg Roach                            self::$waitingText .= $currentLetter;
313d5e02c3aSGreg Roach                            $workingText       = substr($workingText, $currentLen);
314d5e02c3aSGreg Roach                            while (true) {
315d5e02c3aSGreg Roach                                if ($workingText === '') {
316d5e02c3aSGreg Roach                                    break;
317d5e02c3aSGreg Roach                                }
318c5b48766SGreg Roach                                if (str_starts_with($workingText, ' ')) {
319d5e02c3aSGreg Roach                                    // Spaces following this left parenthesis inherit the following directionality too
320d5e02c3aSGreg Roach                                    self::$waitingText .= ' ';
321d5e02c3aSGreg Roach                                    $workingText       = substr($workingText, 1);
322d5e02c3aSGreg Roach                                    continue;
323d5e02c3aSGreg Roach                                }
324c5b48766SGreg Roach                                if (str_starts_with($workingText, '&nbsp;')) {
325d5e02c3aSGreg Roach                                    // Spaces following this left parenthesis inherit the following directionality too
326d5e02c3aSGreg Roach                                    self::$waitingText .= '&nbsp;';
327d5e02c3aSGreg Roach                                    $workingText       = substr($workingText, 6);
328d5e02c3aSGreg Roach                                    continue;
329d5e02c3aSGreg Roach                                }
330d5e02c3aSGreg Roach                                break;
331d5e02c3aSGreg Roach                            }
332d5e02c3aSGreg Roach                            $openParDirection[$openParIndex] = '?';
333d5e02c3aSGreg Roach                            break 2; // double break because we're waiting for more information
334d5e02c3aSGreg Roach                        }
335d5e02c3aSGreg Roach
336d5e02c3aSGreg Roach                        // We have a digit or a "normal" special character.
337d5e02c3aSGreg Roach                        //
338d5e02c3aSGreg Roach                        // When this character is not at the start of the input string, it inherits the preceding directionality;
339d5e02c3aSGreg Roach                        // at the start of the input string, it assumes the following directionality.
340d5e02c3aSGreg Roach                        //
341d5e02c3aSGreg Roach                        // Exceptions to this rule will be handled later during final clean-up.
342d5e02c3aSGreg Roach                        //
343d5e02c3aSGreg Roach                        self::$waitingText .= $currentLetter;
344d5e02c3aSGreg Roach                        $workingText       = substr($workingText, $currentLen);
345d5e02c3aSGreg Roach                        if (self::$currentState !== '') {
346d5e02c3aSGreg Roach                            $result            .= self::$waitingText;
347d5e02c3aSGreg Roach                            self::$waitingText = '';
348d5e02c3aSGreg Roach                        }
349d5e02c3aSGreg Roach                        break 2; // double break because we're waiting for more information
350d5e02c3aSGreg Roach                    }
351d5e02c3aSGreg Roach                    if ($newState !== self::$currentState) {
352d5e02c3aSGreg Roach                        // A direction change has occurred
353d5e02c3aSGreg Roach                        self::finishCurrentSpan($result);
354d5e02c3aSGreg Roach                        self::$previousState = self::$currentState;
355d5e02c3aSGreg Roach                        self::$currentState  = $newState;
356d5e02c3aSGreg Roach                        self::beginCurrentSpan($result);
357d5e02c3aSGreg Roach                    }
358d5e02c3aSGreg Roach                    self::$waitingText .= $currentLetter;
359d5e02c3aSGreg Roach                    $workingText       = substr($workingText, $currentLen);
360d5e02c3aSGreg Roach                    $result            .= self::$waitingText;
361d5e02c3aSGreg Roach                    self::$waitingText = '';
362d5e02c3aSGreg Roach
363d5e02c3aSGreg Roach                    foreach ($openParDirection as $index => $value) {
364d5e02c3aSGreg Roach                        // Since we now know the proper direction, remember it for all waiting opening parentheses
365d5e02c3aSGreg Roach                        if ($value === '?') {
366d5e02c3aSGreg Roach                            $openParDirection[$index] = self::$currentState;
367d5e02c3aSGreg Roach                        }
368d5e02c3aSGreg Roach                    }
369d5e02c3aSGreg Roach
370d5e02c3aSGreg Roach                    break;
371d5e02c3aSGreg Roach            }
372d5e02c3aSGreg Roach        }
373d5e02c3aSGreg Roach
374d5e02c3aSGreg Roach        // We're done. Finish last <span> if necessary
375d5e02c3aSGreg Roach        if ($numberState) {
376d5e02c3aSGreg Roach            if (self::$waitingText === '') {
377d5e02c3aSGreg Roach                if (self::$currentState === 'RTL') {
378d5e02c3aSGreg Roach                    $result .= self::UTF8_PDF;
379d5e02c3aSGreg Roach                }
3806dcdd572SGreg Roach            } elseif (self::$currentState === 'RTL') {
381d5e02c3aSGreg Roach                self::$waitingText .= self::UTF8_PDF;
382d5e02c3aSGreg Roach            }
383d5e02c3aSGreg Roach        }
384d5e02c3aSGreg Roach        self::finishCurrentSpan($result, true);
385d5e02c3aSGreg Roach
386d5e02c3aSGreg Roach        // Get rid of any waiting text
387d5e02c3aSGreg Roach        if (self::$waitingText !== '') {
388d5e02c3aSGreg Roach            if (I18N::direction() === 'rtl' && self::$currentState === 'LTR') {
389d5e02c3aSGreg Roach                $result .= self::START_RTL;
390d5e02c3aSGreg Roach                $result .= self::$waitingText;
391d5e02c3aSGreg Roach                $result .= self::END_RTL;
392d5e02c3aSGreg Roach            } else {
393d5e02c3aSGreg Roach                $result .= self::START_LTR;
394d5e02c3aSGreg Roach                $result .= self::$waitingText;
395d5e02c3aSGreg Roach                $result .= self::END_LTR;
396d5e02c3aSGreg Roach            }
397d5e02c3aSGreg Roach            self::$waitingText = '';
398d5e02c3aSGreg Roach        }
399d5e02c3aSGreg Roach
400d5e02c3aSGreg Roach        // Lastly, do some more cleanups
401d5e02c3aSGreg Roach
402d5e02c3aSGreg Roach        // Move leading RTL numeric strings to following LTR text
403d5e02c3aSGreg Roach        // (this happens when the page direction is RTL and the original text begins with a number and is followed by LTR text)
404d5e02c3aSGreg Roach        while (substr($result, 0, self::LENGTH_START + 3) === self::START_RTL . self::UTF8_LRE) {
405d5e02c3aSGreg Roach            $spanEnd = strpos($result, self::END_RTL . self::START_LTR);
406d5e02c3aSGreg Roach            if ($spanEnd === false) {
407d5e02c3aSGreg Roach                break;
408d5e02c3aSGreg Roach            }
409d5e02c3aSGreg Roach            $textSpan = self::stripLrmRlm(substr($result, self::LENGTH_START + 3, $spanEnd - self::LENGTH_START - 3));
410d5e02c3aSGreg Roach            if (I18N::scriptDirection(I18N::textScript($textSpan)) === 'rtl') {
411d5e02c3aSGreg Roach                break;
412d5e02c3aSGreg Roach            }
413d5e02c3aSGreg Roach            $result = self::START_LTR . substr($result, self::LENGTH_START, $spanEnd - self::LENGTH_START) . substr($result, $spanEnd + self::LENGTH_START + self::LENGTH_END);
414d5e02c3aSGreg Roach            break;
415d5e02c3aSGreg Roach        }
416d5e02c3aSGreg Roach
417d5e02c3aSGreg Roach        // On RTL pages, put trailing "." in RTL numeric strings into its own RTL span
418d5e02c3aSGreg Roach        if (I18N::direction() === 'rtl') {
419d5e02c3aSGreg Roach            $result = str_replace(self::UTF8_PDF . '.' . self::END_RTL, self::UTF8_PDF . self::END_RTL . self::START_RTL . '.' . self::END_RTL, $result);
420d5e02c3aSGreg Roach        }
421d5e02c3aSGreg Roach
422d5e02c3aSGreg Roach        // Trim trailing blanks preceding <br> in LTR text
423d5e02c3aSGreg Roach        while (self::$previousState !== 'RTL') {
424d5e02c3aSGreg Roach            if (str_contains($result, ' <LTRbr>')) {
425d5e02c3aSGreg Roach                $result = str_replace(' <LTRbr>', '<LTRbr>', $result);
426d5e02c3aSGreg Roach                continue;
427d5e02c3aSGreg Roach            }
428d5e02c3aSGreg Roach            if (str_contains($result, '&nbsp;<LTRbr>')) {
429d5e02c3aSGreg Roach                $result = str_replace('&nbsp;<LTRbr>', '<LTRbr>', $result);
430d5e02c3aSGreg Roach                continue;
431d5e02c3aSGreg Roach            }
432d5e02c3aSGreg Roach            if (str_contains($result, ' <br>')) {
433d5e02c3aSGreg Roach                $result = str_replace(' <br>', '<br>', $result);
434d5e02c3aSGreg Roach                continue;
435d5e02c3aSGreg Roach            }
436d5e02c3aSGreg Roach            if (str_contains($result, '&nbsp;<br>')) {
437d5e02c3aSGreg Roach                $result = str_replace('&nbsp;<br>', '<br>', $result);
438d5e02c3aSGreg Roach                continue;
439d5e02c3aSGreg Roach            }
440d5e02c3aSGreg Roach            break; // Neither space nor &nbsp; : we're done
441d5e02c3aSGreg Roach        }
442d5e02c3aSGreg Roach
443d5e02c3aSGreg Roach        // Trim trailing blanks preceding <br> in RTL text
444d5e02c3aSGreg Roach        while (true) {
445d5e02c3aSGreg Roach            if (str_contains($result, ' <RTLbr>')) {
446d5e02c3aSGreg Roach                $result = str_replace(' <RTLbr>', '<RTLbr>', $result);
447d5e02c3aSGreg Roach                continue;
448d5e02c3aSGreg Roach            }
449d5e02c3aSGreg Roach            if (str_contains($result, '&nbsp;<RTLbr>')) {
450d5e02c3aSGreg Roach                $result = str_replace('&nbsp;<RTLbr>', '<RTLbr>', $result);
451d5e02c3aSGreg Roach                continue;
452d5e02c3aSGreg Roach            }
453d5e02c3aSGreg Roach            break; // Neither space nor &nbsp; : we're done
454d5e02c3aSGreg Roach        }
455d5e02c3aSGreg Roach
456d5e02c3aSGreg Roach        // Convert '<LTRbr>' and '<RTLbr'
457d5e02c3aSGreg Roach        $result = str_replace([
458d5e02c3aSGreg Roach            '<LTRbr>',
459d5e02c3aSGreg Roach            '<RTLbr>',
460d5e02c3aSGreg Roach        ], [
461d5e02c3aSGreg Roach            self::END_LTR . '<br>' . self::START_LTR,
462d5e02c3aSGreg Roach            self::END_RTL . '<br>' . self::START_RTL,
463d5e02c3aSGreg Roach        ], $result);
464d5e02c3aSGreg Roach
465d5e02c3aSGreg Roach        // Include leading indeterminate directional text in whatever follows
466c5b48766SGreg Roach        if (substr($result . "\n", 0, self::LENGTH_START) !== self::START_LTR && substr($result . "\n", 0, self::LENGTH_START) !== self::START_RTL && !str_starts_with($result . "\n", '<br>')) {
467d5e02c3aSGreg Roach            $leadingText = '';
468d5e02c3aSGreg Roach            while (true) {
469d5e02c3aSGreg Roach                if ($result === '') {
470d5e02c3aSGreg Roach                    $result = $leadingText;
471d5e02c3aSGreg Roach                    break;
472d5e02c3aSGreg Roach                }
473d5e02c3aSGreg Roach                if (substr($result . "\n", 0, self::LENGTH_START) !== self::START_LTR && substr($result . "\n", 0, self::LENGTH_START) !== self::START_RTL) {
474d5e02c3aSGreg Roach                    $leadingText .= substr($result, 0, 1);
475d5e02c3aSGreg Roach                    $result      = substr($result, 1);
476d5e02c3aSGreg Roach                    continue;
477d5e02c3aSGreg Roach                }
478d5e02c3aSGreg Roach                $result = substr($result, 0, self::LENGTH_START) . $leadingText . substr($result, self::LENGTH_START);
479d5e02c3aSGreg Roach                break;
480d5e02c3aSGreg Roach            }
481d5e02c3aSGreg Roach        }
482d5e02c3aSGreg Roach
483d5e02c3aSGreg Roach        // Include solitary "-" and "+" in surrounding RTL text
484d5e02c3aSGreg Roach        $result = str_replace([
485d5e02c3aSGreg Roach            self::END_RTL . self::START_LTR . '-' . self::END_LTR . self::START_RTL,
486d5e02c3aSGreg Roach            self::END_RTL . self::START_LTR . '+' . self::END_LTR . self::START_RTL,
487d5e02c3aSGreg Roach        ], [
488d5e02c3aSGreg Roach            '-',
489d5e02c3aSGreg Roach            '+',
490d5e02c3aSGreg Roach        ], $result);
491d5e02c3aSGreg Roach
492d5e02c3aSGreg Roach        //$result = strtr($result, [
493d5e02c3aSGreg Roach        //    self::END_RTL . self::START_LTR . '-' . self::END_LTR . self::START_RTL => '-',
494d5e02c3aSGreg Roach        //    self::END_RTL . self::START_LTR . '+' . self::END_LTR . self::START_RTL => '+',
495d5e02c3aSGreg Roach        //]);
496d5e02c3aSGreg Roach
497d5e02c3aSGreg Roach        // Remove empty spans
498d5e02c3aSGreg Roach        $result = str_replace([
499d5e02c3aSGreg Roach            self::START_LTR . self::END_LTR,
500d5e02c3aSGreg Roach            self::START_RTL . self::END_RTL,
501d5e02c3aSGreg Roach        ], '', $result);
502d5e02c3aSGreg Roach
503d5e02c3aSGreg Roach        // Finally, correct '<LTR>', '</LTR>', '<RTL>', and '</RTL>'
504d5e02c3aSGreg Roach        // LTR text: <span dir="ltr"> text </span>
505d5e02c3aSGreg Roach        // RTL text: <span dir="rtl"> text </span>
506d5e02c3aSGreg Roach
507d5e02c3aSGreg Roach        $result = str_replace([
508d5e02c3aSGreg Roach            self::START_LTR,
509d5e02c3aSGreg Roach            self::END_LTR,
510d5e02c3aSGreg Roach            self::START_RTL,
511d5e02c3aSGreg Roach            self::END_RTL,
512d5e02c3aSGreg Roach        ], [
513d5e02c3aSGreg Roach            '<span dir="ltr">',
514d5e02c3aSGreg Roach            '</span>',
515d5e02c3aSGreg Roach            '<span dir="rtl">',
516d5e02c3aSGreg Roach            '</span>',
517d5e02c3aSGreg Roach        ], $result);
518d5e02c3aSGreg Roach
519d5e02c3aSGreg Roach        return $result;
520d5e02c3aSGreg Roach    }
521d5e02c3aSGreg Roach
522d5e02c3aSGreg Roach    /**
523d5e02c3aSGreg Roach     * Wrap words that have an asterisk suffix in <u> and </u> tags.
524d5e02c3aSGreg Roach     * This should underline starred names to show the preferred name.
525d5e02c3aSGreg Roach     *
526d5e02c3aSGreg Roach     * @param string $textSpan
527d5e02c3aSGreg Roach     * @param string $direction
528d5e02c3aSGreg Roach     *
529d5e02c3aSGreg Roach     * @return string
530d5e02c3aSGreg Roach     */
531d5e02c3aSGreg Roach    private static function starredName(string $textSpan, string $direction): string
532d5e02c3aSGreg Roach    {
533d5e02c3aSGreg Roach        // To avoid a TCPDF bug that mixes up the word order, insert those <u> and </u> tags
534d5e02c3aSGreg Roach        // only when page and span directions are identical.
535d5e02c3aSGreg Roach        if ($direction === strtoupper(I18N::direction())) {
536d5e02c3aSGreg Roach            while (true) {
537d5e02c3aSGreg Roach                $starPos = strpos($textSpan, '*');
538d5e02c3aSGreg Roach                if ($starPos === false) {
539d5e02c3aSGreg Roach                    break;
540d5e02c3aSGreg Roach                }
541d5e02c3aSGreg Roach                $trailingText = substr($textSpan, $starPos + 1);
542d5e02c3aSGreg Roach                $textSpan     = substr($textSpan, 0, $starPos);
543d5e02c3aSGreg Roach                $wordStart    = strrpos($textSpan, ' '); // Find the start of the word
544d5e02c3aSGreg Roach                if ($wordStart !== false) {
545d5e02c3aSGreg Roach                    $leadingText = substr($textSpan, 0, $wordStart + 1);
546d5e02c3aSGreg Roach                    $wordText    = substr($textSpan, $wordStart + 1);
547d5e02c3aSGreg Roach                } else {
548d5e02c3aSGreg Roach                    $leadingText = '';
549d5e02c3aSGreg Roach                    $wordText    = $textSpan;
550d5e02c3aSGreg Roach                }
551d5e02c3aSGreg Roach                $textSpan = $leadingText . '<u>' . $wordText . '</u>' . $trailingText;
552d5e02c3aSGreg Roach            }
553d5e02c3aSGreg Roach            $textSpan = preg_replace('~<span class="starredname">(.*)</span>~', '<u>\1</u>', $textSpan);
554d5e02c3aSGreg Roach            // The &nbsp; is a work-around for a TCPDF bug eating blanks.
555d5e02c3aSGreg Roach            $textSpan = str_replace([
556d5e02c3aSGreg Roach                ' <u>',
557d5e02c3aSGreg Roach                '</u> ',
558d5e02c3aSGreg Roach            ], [
559d5e02c3aSGreg Roach                '&nbsp;<u>',
560d5e02c3aSGreg Roach                '</u>&nbsp;',
561d5e02c3aSGreg Roach            ], $textSpan);
562d5e02c3aSGreg Roach        } else {
563d5e02c3aSGreg Roach            // Text and page directions differ:  remove the <span> and </span>
564d5e02c3aSGreg Roach            $textSpan = preg_replace('~(.*)\*~', '\1', $textSpan);
565d5e02c3aSGreg Roach            $textSpan = preg_replace('~<span class="starredname">(.*)</span>~', '\1', $textSpan);
566d5e02c3aSGreg Roach        }
567d5e02c3aSGreg Roach
568d5e02c3aSGreg Roach        return $textSpan;
569d5e02c3aSGreg Roach    }
570d5e02c3aSGreg Roach
571d5e02c3aSGreg Roach    /**
572d5e02c3aSGreg Roach     * Get the next character from an input string
573d5e02c3aSGreg Roach     *
574d5e02c3aSGreg Roach     * @param string $text
575d5e02c3aSGreg Roach     * @param int    $offset
576d5e02c3aSGreg Roach     *
577*bd29d468SGreg Roach     * @return array{letter:string,length:int}
578d5e02c3aSGreg Roach     */
579d5e02c3aSGreg Roach    private static function getChar(string $text, int $offset): array
580d5e02c3aSGreg Roach    {
581d5e02c3aSGreg Roach        if ($text === '') {
582d5e02c3aSGreg Roach            return [
583d5e02c3aSGreg Roach                'letter' => '',
584d5e02c3aSGreg Roach                'length' => 0,
585d5e02c3aSGreg Roach            ];
586d5e02c3aSGreg Roach        }
587d5e02c3aSGreg Roach
588d5e02c3aSGreg Roach        $char   = substr($text, $offset, 1);
589d5e02c3aSGreg Roach        $length = 1;
590d5e02c3aSGreg Roach        if ((ord($char) & 0xE0) === 0xC0) {
591d5e02c3aSGreg Roach            $length = 2;
592d5e02c3aSGreg Roach        }
593d5e02c3aSGreg Roach        if ((ord($char) & 0xF0) === 0xE0) {
594d5e02c3aSGreg Roach            $length = 3;
595d5e02c3aSGreg Roach        }
596d5e02c3aSGreg Roach        if ((ord($char) & 0xF8) === 0xF0) {
597d5e02c3aSGreg Roach            $length = 4;
598d5e02c3aSGreg Roach        }
599d5e02c3aSGreg Roach        $letter = substr($text, $offset, $length);
600d5e02c3aSGreg Roach
601d5e02c3aSGreg Roach        return [
602d5e02c3aSGreg Roach            'letter' => $letter,
603d5e02c3aSGreg Roach            'length' => $length,
604d5e02c3aSGreg Roach        ];
605d5e02c3aSGreg Roach    }
606d5e02c3aSGreg Roach
607d5e02c3aSGreg Roach    /**
608d5e02c3aSGreg Roach     * Insert <br> into current span
609d5e02c3aSGreg Roach     *
610d5e02c3aSGreg Roach     * @param string $result
611d5e02c3aSGreg Roach     *
612d5e02c3aSGreg Roach     * @return void
613d5e02c3aSGreg Roach     */
614d5e02c3aSGreg Roach    private static function breakCurrentSpan(string &$result): void
615d5e02c3aSGreg Roach    {
616d5e02c3aSGreg Roach        // Interrupt the current span, insert that <br>, and then continue the current span
617d5e02c3aSGreg Roach        $result            .= self::$waitingText;
618d5e02c3aSGreg Roach        self::$waitingText = '';
619d5e02c3aSGreg Roach
620d5e02c3aSGreg Roach        $breakString = '<' . self::$currentState . 'br>';
621d5e02c3aSGreg Roach        $result      .= $breakString;
622d5e02c3aSGreg Roach    }
623d5e02c3aSGreg Roach
624d5e02c3aSGreg Roach    /**
625d5e02c3aSGreg Roach     * Begin current span
626d5e02c3aSGreg Roach     *
627d5e02c3aSGreg Roach     * @param string $result
628d5e02c3aSGreg Roach     *
629d5e02c3aSGreg Roach     * @return void
630d5e02c3aSGreg Roach     */
631d5e02c3aSGreg Roach    private static function beginCurrentSpan(string &$result): void
632d5e02c3aSGreg Roach    {
633d5e02c3aSGreg Roach        if (self::$currentState === 'LTR') {
634d5e02c3aSGreg Roach            $result .= self::START_LTR;
635d5e02c3aSGreg Roach        }
636d5e02c3aSGreg Roach        if (self::$currentState === 'RTL') {
637d5e02c3aSGreg Roach            $result .= self::START_RTL;
638d5e02c3aSGreg Roach        }
639d5e02c3aSGreg Roach
640d5e02c3aSGreg Roach        self::$posSpanStart = strlen($result);
641d5e02c3aSGreg Roach    }
642d5e02c3aSGreg Roach
643d5e02c3aSGreg Roach    /**
644d5e02c3aSGreg Roach     * Finish current span
645d5e02c3aSGreg Roach     *
646d5e02c3aSGreg Roach     * @param string $result
647d5e02c3aSGreg Roach     * @param bool   $theEnd
648d5e02c3aSGreg Roach     *
649d5e02c3aSGreg Roach     * @return void
650d5e02c3aSGreg Roach     */
651d5e02c3aSGreg Roach    private static function finishCurrentSpan(string &$result, bool $theEnd = false): void
652d5e02c3aSGreg Roach    {
653d5e02c3aSGreg Roach        $textSpan = substr($result, self::$posSpanStart);
654d5e02c3aSGreg Roach        $result   = substr($result, 0, self::$posSpanStart);
655d5e02c3aSGreg Roach
656d5e02c3aSGreg Roach        // Get rid of empty spans, so that our check for presence of RTL will work
657d5e02c3aSGreg Roach        $result = str_replace([
658d5e02c3aSGreg Roach            self::START_LTR . self::END_LTR,
659d5e02c3aSGreg Roach            self::START_RTL . self::END_RTL,
660d5e02c3aSGreg Roach        ], '', $result);
661d5e02c3aSGreg Roach
662d5e02c3aSGreg Roach        // Look for numeric strings that are times (hh:mm:ss). These have to be separated from surrounding numbers.
663d5e02c3aSGreg Roach        $tempResult = '';
664d5e02c3aSGreg Roach        while ($textSpan !== '') {
665d5e02c3aSGreg Roach            $posColon = strpos($textSpan, ':');
666d5e02c3aSGreg Roach            if ($posColon === false) {
667d5e02c3aSGreg Roach                break;
668d5e02c3aSGreg Roach            } // No more possible time strings
669d5e02c3aSGreg Roach            $posLRE = strpos($textSpan, self::UTF8_LRE);
670d5e02c3aSGreg Roach            if ($posLRE === false) {
671d5e02c3aSGreg Roach                break;
672d5e02c3aSGreg Roach            } // No more numeric strings
673d5e02c3aSGreg Roach            $posPDF = strpos($textSpan, self::UTF8_PDF, $posLRE);
674d5e02c3aSGreg Roach            if ($posPDF === false) {
675d5e02c3aSGreg Roach                break;
676d5e02c3aSGreg Roach            } // No more numeric strings
677d5e02c3aSGreg Roach
678d5e02c3aSGreg Roach            $tempResult    .= substr($textSpan, 0, $posLRE + 3); // Copy everything preceding the numeric string
679d5e02c3aSGreg Roach            $numericString = substr($textSpan, $posLRE + 3, $posPDF - $posLRE); // Separate the entire numeric string
680d5e02c3aSGreg Roach            $textSpan      = substr($textSpan, $posPDF + 3);
681d5e02c3aSGreg Roach            $posColon      = strpos($numericString, ':');
682d5e02c3aSGreg Roach            if ($posColon === false) {
683d5e02c3aSGreg Roach                // Nothing that looks like a time here
684d5e02c3aSGreg Roach                $tempResult .= $numericString;
685d5e02c3aSGreg Roach                continue;
686d5e02c3aSGreg Roach            }
687d5e02c3aSGreg Roach            $posBlank = strpos($numericString . ' ', ' ');
688d5e02c3aSGreg Roach            $posNbsp  = strpos($numericString . '&nbsp;', '&nbsp;');
689d5e02c3aSGreg Roach            if ($posBlank < $posNbsp) {
690d5e02c3aSGreg Roach                $posSeparator    = $posBlank;
691d5e02c3aSGreg Roach                $lengthSeparator = 1;
692d5e02c3aSGreg Roach            } else {
693d5e02c3aSGreg Roach                $posSeparator    = $posNbsp;
694d5e02c3aSGreg Roach                $lengthSeparator = 6;
695d5e02c3aSGreg Roach            }
696d5e02c3aSGreg Roach            if ($posColon > $posSeparator) {
697d5e02c3aSGreg Roach                // We have a time string preceded by a blank: Exclude that blank from the numeric string
698d5e02c3aSGreg Roach                $tempResult    .= substr($numericString, 0, $posSeparator);
699d5e02c3aSGreg Roach                $tempResult    .= self::UTF8_PDF;
700d5e02c3aSGreg Roach                $tempResult    .= substr($numericString, $posSeparator, $lengthSeparator);
701d5e02c3aSGreg Roach                $tempResult    .= self::UTF8_LRE;
702d5e02c3aSGreg Roach                $numericString = substr($numericString, $posSeparator + $lengthSeparator);
703d5e02c3aSGreg Roach            }
704d5e02c3aSGreg Roach
705d5e02c3aSGreg Roach            $posBlank = strpos($numericString, ' ');
706d5e02c3aSGreg Roach            $posNbsp  = strpos($numericString, '&nbsp;');
707d5e02c3aSGreg Roach            if ($posBlank === false && $posNbsp === false) {
708d5e02c3aSGreg Roach                // The time string isn't followed by a blank
709d5e02c3aSGreg Roach                $textSpan = $numericString . $textSpan;
710d5e02c3aSGreg Roach                continue;
711d5e02c3aSGreg Roach            }
712d5e02c3aSGreg Roach
713d5e02c3aSGreg Roach            // We have a time string followed by a blank: Exclude that blank from the numeric string
714d5e02c3aSGreg Roach            if ($posBlank === false) {
715d5e02c3aSGreg Roach                $posSeparator    = $posNbsp;
716d5e02c3aSGreg Roach                $lengthSeparator = 6;
717d5e02c3aSGreg Roach            } elseif ($posNbsp === false) {
718d5e02c3aSGreg Roach                $posSeparator    = $posBlank;
719d5e02c3aSGreg Roach                $lengthSeparator = 1;
720d5e02c3aSGreg Roach            } elseif ($posBlank < $posNbsp) {
721d5e02c3aSGreg Roach                $posSeparator    = $posBlank;
722d5e02c3aSGreg Roach                $lengthSeparator = 1;
723d5e02c3aSGreg Roach            } else {
724d5e02c3aSGreg Roach                $posSeparator    = $posNbsp;
725d5e02c3aSGreg Roach                $lengthSeparator = 6;
726d5e02c3aSGreg Roach            }
727d5e02c3aSGreg Roach            $tempResult    .= substr($numericString, 0, $posSeparator);
728d5e02c3aSGreg Roach            $tempResult    .= self::UTF8_PDF;
729d5e02c3aSGreg Roach            $tempResult    .= substr($numericString, $posSeparator, $lengthSeparator);
730d5e02c3aSGreg Roach            $posSeparator  += $lengthSeparator;
731d5e02c3aSGreg Roach            $numericString = substr($numericString, $posSeparator);
732d5e02c3aSGreg Roach            $textSpan      = self::UTF8_LRE . $numericString . $textSpan;
733d5e02c3aSGreg Roach        }
734d5e02c3aSGreg Roach        $textSpan       = $tempResult . $textSpan;
735d5e02c3aSGreg Roach        $trailingBlanks = '';
736d5e02c3aSGreg Roach        $trailingBreaks = '';
737d5e02c3aSGreg Roach
738d5e02c3aSGreg Roach        /* ****************************** LTR text handling ******************************** */
739d5e02c3aSGreg Roach
740d5e02c3aSGreg Roach        if (self::$currentState === 'LTR') {
741d5e02c3aSGreg Roach            // Move trailing numeric strings to the following RTL text. Include any blanks preceding or following the numeric text too.
742d5e02c3aSGreg Roach            if (I18N::direction() === 'rtl' && self::$previousState === 'RTL' && !$theEnd) {
743d5e02c3aSGreg Roach                $trailingString = '';
744d5e02c3aSGreg Roach                $savedSpan      = $textSpan;
745d5e02c3aSGreg Roach                while ($textSpan !== '') {
746d5e02c3aSGreg Roach                    // Look for trailing spaces and tentatively move them
747c5b48766SGreg Roach                    if (str_ends_with($textSpan, ' ')) {
748d5e02c3aSGreg Roach                        $trailingString = ' ' . $trailingString;
749d5e02c3aSGreg Roach                        $textSpan       = substr($textSpan, 0, -1);
750d5e02c3aSGreg Roach                        continue;
751d5e02c3aSGreg Roach                    }
752c5b48766SGreg Roach                    if (str_ends_with($textSpan, '&nbsp;')) {
753d5e02c3aSGreg Roach                        $trailingString = '&nbsp;' . $trailingString;
754d5e02c3aSGreg Roach                        $textSpan       = substr($textSpan, 0, -1);
755d5e02c3aSGreg Roach                        continue;
756d5e02c3aSGreg Roach                    }
757d5e02c3aSGreg Roach                    if (substr($textSpan, -3) !== self::UTF8_PDF) {
758d5e02c3aSGreg Roach                        // There is no trailing numeric string
759d5e02c3aSGreg Roach                        $textSpan = $savedSpan;
760d5e02c3aSGreg Roach                        break;
761d5e02c3aSGreg Roach                    }
762d5e02c3aSGreg Roach
763d5e02c3aSGreg Roach                    // We have a numeric string
764d5e02c3aSGreg Roach                    $posStartNumber = strrpos($textSpan, self::UTF8_LRE);
765d5e02c3aSGreg Roach                    if ($posStartNumber === false) {
766d5e02c3aSGreg Roach                        $posStartNumber = 0;
767d5e02c3aSGreg Roach                    }
768d5e02c3aSGreg Roach                    $trailingString = substr($textSpan, $posStartNumber) . $trailingString;
769d5e02c3aSGreg Roach                    $textSpan       = substr($textSpan, 0, $posStartNumber);
770d5e02c3aSGreg Roach
771d5e02c3aSGreg Roach                    // Look for more spaces and move them too
772d5e02c3aSGreg Roach                    while ($textSpan !== '') {
773c5b48766SGreg Roach                        if (str_ends_with($textSpan, ' ')) {
774d5e02c3aSGreg Roach                            $trailingString = ' ' . $trailingString;
775d5e02c3aSGreg Roach                            $textSpan       = substr($textSpan, 0, -1);
776d5e02c3aSGreg Roach                            continue;
777d5e02c3aSGreg Roach                        }
778c5b48766SGreg Roach                        if (str_ends_with($textSpan, '&nbsp;')) {
779d5e02c3aSGreg Roach                            $trailingString = '&nbsp;' . $trailingString;
780d5e02c3aSGreg Roach                            $textSpan       = substr($textSpan, 0, -1);
781d5e02c3aSGreg Roach                            continue;
782d5e02c3aSGreg Roach                        }
783d5e02c3aSGreg Roach                        break;
784d5e02c3aSGreg Roach                    }
785d5e02c3aSGreg Roach
786d5e02c3aSGreg Roach                    self::$waitingText = $trailingString . self::$waitingText;
787d5e02c3aSGreg Roach                    break;
788d5e02c3aSGreg Roach                }
789d5e02c3aSGreg Roach            }
790d5e02c3aSGreg Roach
791d5e02c3aSGreg Roach            $savedSpan = $textSpan;
792d5e02c3aSGreg Roach            // Move any trailing <br>, optionally preceded or followed by blanks, outside this LTR span
793d5e02c3aSGreg Roach            while ($textSpan !== '') {
794c5b48766SGreg Roach                if (str_ends_with($textSpan, ' ')) {
795d5e02c3aSGreg Roach                    $trailingBlanks = ' ' . $trailingBlanks;
796d5e02c3aSGreg Roach                    $textSpan       = substr($textSpan, 0, -1);
797d5e02c3aSGreg Roach                    continue;
798d5e02c3aSGreg Roach                }
799c5b48766SGreg Roach                if (str_ends_with('......' . $textSpan, '&nbsp;')) {
800d5e02c3aSGreg Roach                    $trailingBlanks = '&nbsp;' . $trailingBlanks;
801d5e02c3aSGreg Roach                    $textSpan       = substr($textSpan, 0, -6);
802d5e02c3aSGreg Roach                    continue;
803d5e02c3aSGreg Roach                }
804d5e02c3aSGreg Roach                break;
805d5e02c3aSGreg Roach            }
806c5b48766SGreg Roach            while (str_ends_with($textSpan, '<LTRbr>')) {
807d5e02c3aSGreg Roach                $trailingBreaks = '<br>' . $trailingBreaks; // Plain <br> because it’s outside a span
808d5e02c3aSGreg Roach                $textSpan       = substr($textSpan, 0, -7);
809d5e02c3aSGreg Roach            }
810d5e02c3aSGreg Roach            if ($trailingBreaks !== '') {
811d5e02c3aSGreg Roach                while ($textSpan !== '') {
812c5b48766SGreg Roach                    if (str_ends_with($textSpan, ' ')) {
813d5e02c3aSGreg Roach                        $trailingBreaks = ' ' . $trailingBreaks;
814d5e02c3aSGreg Roach                        $textSpan       = substr($textSpan, 0, -1);
815d5e02c3aSGreg Roach                        continue;
816d5e02c3aSGreg Roach                    }
817c5b48766SGreg Roach                    if (str_ends_with($textSpan, '&nbsp;')) {
818d5e02c3aSGreg Roach                        $trailingBreaks = '&nbsp;' . $trailingBreaks;
819d5e02c3aSGreg Roach                        $textSpan       = substr($textSpan, 0, -6);
820d5e02c3aSGreg Roach                        continue;
821d5e02c3aSGreg Roach                    }
822d5e02c3aSGreg Roach                    break;
823d5e02c3aSGreg Roach                }
824d5e02c3aSGreg Roach                self::$waitingText = $trailingBlanks . self::$waitingText; // Put those trailing blanks inside the following span
825d5e02c3aSGreg Roach            } else {
826d5e02c3aSGreg Roach                $textSpan = $savedSpan;
827d5e02c3aSGreg Roach            }
828d5e02c3aSGreg Roach
829d5e02c3aSGreg Roach            $trailingBlanks      = '';
830d5e02c3aSGreg Roach            $trailingPunctuation = '';
831d5e02c3aSGreg Roach            $trailingID          = '';
832d5e02c3aSGreg Roach            $trailingSeparator   = '';
833d5e02c3aSGreg Roach            $leadingSeparator    = '';
834d5e02c3aSGreg Roach
835d5e02c3aSGreg Roach            while (I18N::direction() === 'rtl') {
836d5e02c3aSGreg Roach                if (str_contains($result, self::START_RTL)) {
837d5e02c3aSGreg Roach                    // Remove trailing blanks for inclusion in a separate LTR span
838d5e02c3aSGreg Roach                    while ($textSpan !== '') {
839c5b48766SGreg Roach                        if (str_ends_with($textSpan, ' ')) {
840d5e02c3aSGreg Roach                            $trailingBlanks = ' ' . $trailingBlanks;
841d5e02c3aSGreg Roach                            $textSpan       = substr($textSpan, 0, -1);
842d5e02c3aSGreg Roach                            continue;
843d5e02c3aSGreg Roach                        }
844c5b48766SGreg Roach                        if (str_ends_with($textSpan, '&nbsp;')) {
845d5e02c3aSGreg Roach                            $trailingBlanks = '&nbsp;' . $trailingBlanks;
846d5e02c3aSGreg Roach                            $textSpan       = substr($textSpan, 0, -1);
847d5e02c3aSGreg Roach                            continue;
848d5e02c3aSGreg Roach                        }
849d5e02c3aSGreg Roach                        break;
850d5e02c3aSGreg Roach                    }
851d5e02c3aSGreg Roach
852d5e02c3aSGreg Roach                    // Remove trailing punctuation for inclusion in a separate LTR span
853d5e02c3aSGreg Roach                    if ($textSpan === '') {
854d5e02c3aSGreg Roach                        $trailingChar = "\n";
855d5e02c3aSGreg Roach                    } else {
856d5e02c3aSGreg Roach                        $trailingChar = substr($textSpan, -1);
857d5e02c3aSGreg Roach                    }
858d5e02c3aSGreg Roach                    if (str_contains(self::PUNCTUATION, $trailingChar)) {
859d5e02c3aSGreg Roach                        $trailingPunctuation = $trailingChar;
860d5e02c3aSGreg Roach                        $textSpan            = substr($textSpan, 0, -1);
861d5e02c3aSGreg Roach                    }
862d5e02c3aSGreg Roach                }
863d5e02c3aSGreg Roach
864d5e02c3aSGreg Roach                // Remove trailing ID numbers that look like "(xnnn)" for inclusion in a separate LTR span
865d5e02c3aSGreg Roach                while (true) {
866c5b48766SGreg Roach                    if (!str_ends_with($textSpan, ')')) {
867d5e02c3aSGreg Roach                        break;
868d5e02c3aSGreg Roach                    } // There is no trailing ')'
869d5e02c3aSGreg Roach                    $posLeftParen = strrpos($textSpan, '(');
870d5e02c3aSGreg Roach                    if ($posLeftParen === false) {
871d5e02c3aSGreg Roach                        break;
872d5e02c3aSGreg Roach                    } // There is no leading '('
873d5e02c3aSGreg Roach                    $temp = self::stripLrmRlm(substr($textSpan, $posLeftParen)); // Get rid of UTF8 control codes
874d5e02c3aSGreg Roach
875d5e02c3aSGreg Roach                    // If the parenthesized text doesn't look like an ID number,
876d5e02c3aSGreg Roach                    // we don't want to touch it.
877d5e02c3aSGreg Roach                    // This check won’t work if somebody uses ID numbers with an unusual format.
878d5e02c3aSGreg Roach                    $offset    = 1;
879d5e02c3aSGreg Roach                    $charArray = self::getChar($temp, $offset); // Get 1st character of parenthesized text
880d5e02c3aSGreg Roach                    if (str_contains(self::NUMBERS, $charArray['letter'])) {
881d5e02c3aSGreg Roach                        break;
882d5e02c3aSGreg Roach                    }
883d5e02c3aSGreg Roach                    $offset += $charArray['length']; // Point at 2nd character of parenthesized text
884d5e02c3aSGreg Roach                    if (!str_contains(self::NUMBERS, substr($temp, $offset, 1))) {
885d5e02c3aSGreg Roach                        break;
886d5e02c3aSGreg Roach                    }
887d5e02c3aSGreg Roach                    // 1st character of parenthesized text is alpha, 2nd character is a digit; last has to be a digit too
888d5e02c3aSGreg Roach                    if (!str_contains(self::NUMBERS, substr($temp, -2, 1))) {
889d5e02c3aSGreg Roach                        break;
890d5e02c3aSGreg Roach                    }
891d5e02c3aSGreg Roach
892d5e02c3aSGreg Roach                    $trailingID = substr($textSpan, $posLeftParen);
893d5e02c3aSGreg Roach                    $textSpan   = substr($textSpan, 0, $posLeftParen);
894d5e02c3aSGreg Roach                    break;
895d5e02c3aSGreg Roach                }
896d5e02c3aSGreg Roach
897d5e02c3aSGreg Roach                // Look for " - " or blank preceding the ID number and remove it for inclusion in a separate LTR span
898d5e02c3aSGreg Roach                if ($trailingID !== '') {
899d5e02c3aSGreg Roach                    while ($textSpan !== '') {
900c5b48766SGreg Roach                        if (str_ends_with($textSpan, ' ')) {
901d5e02c3aSGreg Roach                            $trailingSeparator = ' ' . $trailingSeparator;
902d5e02c3aSGreg Roach                            $textSpan          = substr($textSpan, 0, -1);
903d5e02c3aSGreg Roach                            continue;
904d5e02c3aSGreg Roach                        }
905c5b48766SGreg Roach                        if (str_ends_with($textSpan, '&nbsp;')) {
906d5e02c3aSGreg Roach                            $trailingSeparator = '&nbsp;' . $trailingSeparator;
907d5e02c3aSGreg Roach                            $textSpan          = substr($textSpan, 0, -6);
908d5e02c3aSGreg Roach                            continue;
909d5e02c3aSGreg Roach                        }
910c5b48766SGreg Roach                        if (str_ends_with($textSpan, '-')) {
911d5e02c3aSGreg Roach                            $trailingSeparator = '-' . $trailingSeparator;
912d5e02c3aSGreg Roach                            $textSpan          = substr($textSpan, 0, -1);
913d5e02c3aSGreg Roach                            continue;
914d5e02c3aSGreg Roach                        }
915d5e02c3aSGreg Roach                        break;
916d5e02c3aSGreg Roach                    }
917d5e02c3aSGreg Roach                }
918d5e02c3aSGreg Roach
919d5e02c3aSGreg Roach                // Look for " - " preceding the text and remove it for inclusion in a separate LTR span
920d5e02c3aSGreg Roach                $foundSeparator = false;
921d5e02c3aSGreg Roach                $savedSpan      = $textSpan;
922d5e02c3aSGreg Roach                while ($textSpan !== '') {
923c5b48766SGreg Roach                    if (str_starts_with($textSpan, ' ')) {
924d5e02c3aSGreg Roach                        $leadingSeparator = ' ' . $leadingSeparator;
925d5e02c3aSGreg Roach                        $textSpan         = substr($textSpan, 1);
926d5e02c3aSGreg Roach                        continue;
927d5e02c3aSGreg Roach                    }
928c5b48766SGreg Roach                    if (str_starts_with($textSpan, '&nbsp;')) {
929d5e02c3aSGreg Roach                        $leadingSeparator = '&nbsp;' . $leadingSeparator;
930d5e02c3aSGreg Roach                        $textSpan         = substr($textSpan, 6);
931d5e02c3aSGreg Roach                        continue;
932d5e02c3aSGreg Roach                    }
933c5b48766SGreg Roach                    if (str_starts_with($textSpan, '-')) {
934d5e02c3aSGreg Roach                        $leadingSeparator = '-' . $leadingSeparator;
935d5e02c3aSGreg Roach                        $textSpan         = substr($textSpan, 1);
936d5e02c3aSGreg Roach                        $foundSeparator   = true;
937d5e02c3aSGreg Roach                        continue;
938d5e02c3aSGreg Roach                    }
939d5e02c3aSGreg Roach                    break;
940d5e02c3aSGreg Roach                }
941d5e02c3aSGreg Roach                if (!$foundSeparator) {
942d5e02c3aSGreg Roach                    $textSpan         = $savedSpan;
943d5e02c3aSGreg Roach                    $leadingSeparator = '';
944d5e02c3aSGreg Roach                }
945d5e02c3aSGreg Roach                break;
946d5e02c3aSGreg Roach            }
947d5e02c3aSGreg Roach
948d5e02c3aSGreg Roach            // We're done: finish the span
949d5e02c3aSGreg Roach            $textSpan = self::starredName($textSpan, 'LTR'); // Wrap starred name in <u> and </u> tags
950d5e02c3aSGreg Roach            while (true) {
951d5e02c3aSGreg Roach                // Remove blanks that precede <LTRbr>
952d5e02c3aSGreg Roach                if (str_contains($textSpan, ' <LTRbr>')) {
953d5e02c3aSGreg Roach                    $textSpan = str_replace(' <LTRbr>', '<LTRbr>', $textSpan);
954d5e02c3aSGreg Roach                    continue;
955d5e02c3aSGreg Roach                }
956d5e02c3aSGreg Roach                if (str_contains($textSpan, '&nbsp;<LTRbr>')) {
957d5e02c3aSGreg Roach                    $textSpan = str_replace('&nbsp;<LTRbr>', '<LTRbr>', $textSpan);
958d5e02c3aSGreg Roach                    continue;
959d5e02c3aSGreg Roach                }
960d5e02c3aSGreg Roach                break;
961d5e02c3aSGreg Roach            }
962d5e02c3aSGreg Roach            if ($leadingSeparator !== '') {
963d5e02c3aSGreg Roach                $result .= self::START_LTR . $leadingSeparator . self::END_LTR;
964d5e02c3aSGreg Roach            }
965d5e02c3aSGreg Roach            $result .= $textSpan . self::END_LTR;
966d5e02c3aSGreg Roach            if ($trailingSeparator !== '') {
967d5e02c3aSGreg Roach                $result .= self::START_LTR . $trailingSeparator . self::END_LTR;
968d5e02c3aSGreg Roach            }
969d5e02c3aSGreg Roach            if ($trailingID !== '') {
970d5e02c3aSGreg Roach                $result .= self::START_LTR . $trailingID . self::END_LTR;
971d5e02c3aSGreg Roach            }
972d5e02c3aSGreg Roach            if ($trailingPunctuation !== '') {
973d5e02c3aSGreg Roach                $result .= self::START_LTR . $trailingPunctuation . self::END_LTR;
974d5e02c3aSGreg Roach            }
975d5e02c3aSGreg Roach            if ($trailingBlanks !== '') {
976d5e02c3aSGreg Roach                $result .= self::START_LTR . $trailingBlanks . self::END_LTR;
977d5e02c3aSGreg Roach            }
978d5e02c3aSGreg Roach        }
979d5e02c3aSGreg Roach
980d5e02c3aSGreg Roach        /* ****************************** RTL text handling ******************************** */
981d5e02c3aSGreg Roach
982d5e02c3aSGreg Roach        if (self::$currentState === 'RTL') {
983d5e02c3aSGreg Roach            $savedSpan = $textSpan;
984d5e02c3aSGreg Roach
985d5e02c3aSGreg Roach            // Move any trailing <br>, optionally followed by blanks, outside this RTL span
986d5e02c3aSGreg Roach            while ($textSpan !== '') {
987c5b48766SGreg Roach                if (str_ends_with($textSpan, ' ')) {
988d5e02c3aSGreg Roach                    $trailingBlanks = ' ' . $trailingBlanks;
989d5e02c3aSGreg Roach                    $textSpan       = substr($textSpan, 0, -1);
990d5e02c3aSGreg Roach                    continue;
991d5e02c3aSGreg Roach                }
992c5b48766SGreg Roach                if (str_ends_with('......' . $textSpan, '&nbsp;')) {
993d5e02c3aSGreg Roach                    $trailingBlanks = '&nbsp;' . $trailingBlanks;
994d5e02c3aSGreg Roach                    $textSpan       = substr($textSpan, 0, -6);
995d5e02c3aSGreg Roach                    continue;
996d5e02c3aSGreg Roach                }
997d5e02c3aSGreg Roach                break;
998d5e02c3aSGreg Roach            }
999c5b48766SGreg Roach            while (str_ends_with($textSpan, '<RTLbr>')) {
1000d5e02c3aSGreg Roach                $trailingBreaks = '<br>' . $trailingBreaks; // Plain <br> because it’s outside a span
1001d5e02c3aSGreg Roach                $textSpan       = substr($textSpan, 0, -7);
1002d5e02c3aSGreg Roach            }
1003d5e02c3aSGreg Roach            if ($trailingBreaks !== '') {
1004d5e02c3aSGreg Roach                self::$waitingText = $trailingBlanks . self::$waitingText; // Put those trailing blanks inside the following span
1005d5e02c3aSGreg Roach            } else {
1006d5e02c3aSGreg Roach                $textSpan = $savedSpan;
1007d5e02c3aSGreg Roach            }
1008d5e02c3aSGreg Roach
1009d5e02c3aSGreg Roach            // Move trailing numeric strings to the following LTR text. Include any blanks preceding or following the numeric text too.
1010d5e02c3aSGreg Roach            if (!$theEnd && I18N::direction() !== 'rtl') {
1011d5e02c3aSGreg Roach                $trailingString = '';
1012d5e02c3aSGreg Roach                $savedSpan      = $textSpan;
1013d5e02c3aSGreg Roach                while ($textSpan !== '') {
1014d5e02c3aSGreg Roach                    // Look for trailing spaces and tentatively move them
1015c5b48766SGreg Roach                    if (str_ends_with($textSpan, ' ')) {
1016d5e02c3aSGreg Roach                        $trailingString = ' ' . $trailingString;
1017d5e02c3aSGreg Roach                        $textSpan       = substr($textSpan, 0, -1);
1018d5e02c3aSGreg Roach                        continue;
1019d5e02c3aSGreg Roach                    }
1020c5b48766SGreg Roach                    if (str_ends_with($textSpan, '&nbsp;')) {
1021d5e02c3aSGreg Roach                        $trailingString = '&nbsp;' . $trailingString;
1022d5e02c3aSGreg Roach                        $textSpan       = substr($textSpan, 0, -1);
1023d5e02c3aSGreg Roach                        continue;
1024d5e02c3aSGreg Roach                    }
1025d5e02c3aSGreg Roach                    if (substr($textSpan, -3) !== self::UTF8_PDF) {
1026d5e02c3aSGreg Roach                        // There is no trailing numeric string
1027d5e02c3aSGreg Roach                        $textSpan = $savedSpan;
1028d5e02c3aSGreg Roach                        break;
1029d5e02c3aSGreg Roach                    }
1030d5e02c3aSGreg Roach
1031d5e02c3aSGreg Roach                    // We have a numeric string
1032d5e02c3aSGreg Roach                    $posStartNumber = strrpos($textSpan, self::UTF8_LRE);
1033d5e02c3aSGreg Roach                    if ($posStartNumber === false) {
1034d5e02c3aSGreg Roach                        $posStartNumber = 0;
1035d5e02c3aSGreg Roach                    }
1036d5e02c3aSGreg Roach                    $trailingString = substr($textSpan, $posStartNumber) . $trailingString;
1037d5e02c3aSGreg Roach                    $textSpan       = substr($textSpan, 0, $posStartNumber);
1038d5e02c3aSGreg Roach
1039d5e02c3aSGreg Roach                    // Look for more spaces and move them too
1040d5e02c3aSGreg Roach                    while ($textSpan !== '') {
1041c5b48766SGreg Roach                        if (str_ends_with($textSpan, ' ')) {
1042d5e02c3aSGreg Roach                            $trailingString = ' ' . $trailingString;
1043d5e02c3aSGreg Roach                            $textSpan       = substr($textSpan, 0, -1);
1044d5e02c3aSGreg Roach                            continue;
1045d5e02c3aSGreg Roach                        }
1046c5b48766SGreg Roach                        if (str_ends_with($textSpan, '&nbsp;')) {
1047d5e02c3aSGreg Roach                            $trailingString = '&nbsp;' . $trailingString;
1048d5e02c3aSGreg Roach                            $textSpan       = substr($textSpan, 0, -1);
1049d5e02c3aSGreg Roach                            continue;
1050d5e02c3aSGreg Roach                        }
1051d5e02c3aSGreg Roach                        break;
1052d5e02c3aSGreg Roach                    }
1053d5e02c3aSGreg Roach
1054d5e02c3aSGreg Roach                    self::$waitingText = $trailingString . self::$waitingText;
1055d5e02c3aSGreg Roach                    break;
1056d5e02c3aSGreg Roach                }
1057d5e02c3aSGreg Roach            }
1058d5e02c3aSGreg Roach
1059d5e02c3aSGreg Roach            // Trailing " - " needs to be prefixed to the following span
1060c5b48766SGreg Roach            if (!$theEnd && str_ends_with('...' . $textSpan, ' - ')) {
1061d5e02c3aSGreg Roach                $textSpan          = substr($textSpan, 0, -3);
1062d5e02c3aSGreg Roach                self::$waitingText = ' - ' . self::$waitingText;
1063d5e02c3aSGreg Roach            }
1064d5e02c3aSGreg Roach
1065d5e02c3aSGreg Roach            while (I18N::direction() === 'rtl') {
1066d5e02c3aSGreg Roach                // Look for " - " preceding <RTLbr> and relocate it to the front of the string
1067d5e02c3aSGreg Roach                $posDashString = strpos($textSpan, ' - <RTLbr>');
1068d5e02c3aSGreg Roach                if ($posDashString === false) {
1069d5e02c3aSGreg Roach                    break;
1070d5e02c3aSGreg Roach                }
1071d5e02c3aSGreg Roach                $posStringStart = strrpos(substr($textSpan, 0, $posDashString), '<RTLbr>');
1072d5e02c3aSGreg Roach                if ($posStringStart === false) {
1073d5e02c3aSGreg Roach                    $posStringStart = 0;
1074d5e02c3aSGreg Roach                } else {
1075d5e02c3aSGreg Roach                    $posStringStart += 9;
1076d5e02c3aSGreg Roach                } // Point to the first char following the last <RTLbr>
1077d5e02c3aSGreg Roach
1078d5e02c3aSGreg Roach                $textSpan = substr($textSpan, 0, $posStringStart) . ' - ' . substr($textSpan, $posStringStart, $posDashString - $posStringStart) . substr($textSpan, $posDashString + 3);
1079d5e02c3aSGreg Roach            }
1080d5e02c3aSGreg Roach
1081d5e02c3aSGreg Roach            // Strip leading spaces from the RTL text
1082d5e02c3aSGreg Roach            $countLeadingSpaces = 0;
1083d5e02c3aSGreg Roach            while ($textSpan !== '') {
1084c5b48766SGreg Roach                if (str_starts_with($textSpan, ' ')) {
1085d5e02c3aSGreg Roach                    $countLeadingSpaces++;
1086d5e02c3aSGreg Roach                    $textSpan = substr($textSpan, 1);
1087d5e02c3aSGreg Roach                    continue;
1088d5e02c3aSGreg Roach                }
1089c5b48766SGreg Roach                if (str_starts_with($textSpan, '&nbsp;')) {
1090d5e02c3aSGreg Roach                    $countLeadingSpaces++;
1091d5e02c3aSGreg Roach                    $textSpan = substr($textSpan, 6);
1092d5e02c3aSGreg Roach                    continue;
1093d5e02c3aSGreg Roach                }
1094d5e02c3aSGreg Roach                break;
1095d5e02c3aSGreg Roach            }
1096d5e02c3aSGreg Roach
1097d5e02c3aSGreg Roach            // Strip trailing spaces from the RTL text
1098d5e02c3aSGreg Roach            $countTrailingSpaces = 0;
1099d5e02c3aSGreg Roach            while ($textSpan !== '') {
1100c5b48766SGreg Roach                if (str_ends_with($textSpan, ' ')) {
1101d5e02c3aSGreg Roach                    $countTrailingSpaces++;
1102d5e02c3aSGreg Roach                    $textSpan = substr($textSpan, 0, -1);
1103d5e02c3aSGreg Roach                    continue;
1104d5e02c3aSGreg Roach                }
1105c5b48766SGreg Roach                if (str_ends_with($textSpan, '&nbsp;')) {
1106d5e02c3aSGreg Roach                    $countTrailingSpaces++;
1107d5e02c3aSGreg Roach                    $textSpan = substr($textSpan, 0, -6);
1108d5e02c3aSGreg Roach                    continue;
1109d5e02c3aSGreg Roach                }
1110d5e02c3aSGreg Roach                break;
1111d5e02c3aSGreg Roach            }
1112d5e02c3aSGreg Roach
1113d5e02c3aSGreg Roach            // Look for trailing " -", reverse it, and relocate it to the front of the string
1114c5b48766SGreg Roach            if (str_ends_with($textSpan, ' -')) {
1115d5e02c3aSGreg Roach                $posDashString  = strlen($textSpan) - 2;
1116d5e02c3aSGreg Roach                $posStringStart = strrpos(substr($textSpan, 0, $posDashString), '<RTLbr>');
1117d5e02c3aSGreg Roach                if ($posStringStart === false) {
1118d5e02c3aSGreg Roach                    $posStringStart = 0;
1119d5e02c3aSGreg Roach                } else {
1120d5e02c3aSGreg Roach                    $posStringStart += 9;
1121d5e02c3aSGreg Roach                } // Point to the first char following the last <RTLbr>
1122d5e02c3aSGreg Roach
1123d5e02c3aSGreg Roach                $textSpan = substr($textSpan, 0, $posStringStart) . '- ' . substr($textSpan, $posStringStart, $posDashString - $posStringStart) . substr($textSpan, $posDashString + 2);
1124d5e02c3aSGreg Roach            }
1125d5e02c3aSGreg Roach
1126d5e02c3aSGreg Roach            if ($countLeadingSpaces !== 0) {
1127d5e02c3aSGreg Roach                $newLength = strlen($textSpan) + $countLeadingSpaces;
1128d5e02c3aSGreg Roach                $textSpan  = str_pad($textSpan, $newLength, ' ', I18N::direction() === 'rtl' ? STR_PAD_LEFT : STR_PAD_RIGHT);
1129d5e02c3aSGreg Roach            }
1130d5e02c3aSGreg Roach            if ($countTrailingSpaces !== 0) {
1131d5e02c3aSGreg Roach                if (I18N::direction() === 'ltr') {
1132d5e02c3aSGreg Roach                    if ($trailingBreaks === '') {
1133d5e02c3aSGreg Roach                        // Move trailing RTL spaces to front of following LTR span
1134d5e02c3aSGreg Roach                        $newLength         = strlen(self::$waitingText) + $countTrailingSpaces;
1135d5e02c3aSGreg Roach                        self::$waitingText = str_pad(self::$waitingText, $newLength, ' ', STR_PAD_LEFT);
1136d5e02c3aSGreg Roach                    }
1137d5e02c3aSGreg Roach                } else {
1138d5e02c3aSGreg Roach                    $newLength = strlen($textSpan) + $countTrailingSpaces;
1139d5e02c3aSGreg Roach                    $textSpan  = str_pad($textSpan, $newLength);
1140d5e02c3aSGreg Roach                }
1141d5e02c3aSGreg Roach            }
1142d5e02c3aSGreg Roach
1143d5e02c3aSGreg Roach            // We're done: finish the span
1144d5e02c3aSGreg Roach            $textSpan = self::starredName($textSpan, 'RTL'); // Wrap starred name in <u> and </u> tags
1145d5e02c3aSGreg Roach            $result   .= $textSpan . self::END_RTL;
1146d5e02c3aSGreg Roach        }
1147d5e02c3aSGreg Roach
1148d5e02c3aSGreg Roach        if (self::$currentState !== 'LTR' && self::$currentState !== 'RTL') {
1149d5e02c3aSGreg Roach            $result .= $textSpan;
1150d5e02c3aSGreg Roach        }
1151d5e02c3aSGreg Roach
1152d5e02c3aSGreg Roach        $result .= $trailingBreaks; // Get rid of any waiting <br>
1153d5e02c3aSGreg Roach    }
1154d5e02c3aSGreg Roach}
1155