xref: /webtrees/app/Report/RightToLeftSupport.php (revision 7ba3f06a11471601665c0c64c793435a69fd819e)
1d5e02c3aSGreg Roach<?php
2d5e02c3aSGreg Roach
3d5e02c3aSGreg Roach/**
4d5e02c3aSGreg Roach * webtrees: online genealogy
5*d11be702SGreg Roach * Copyright (C) 2023 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                        self::$waitingText .= $currentLetter;
311d5e02c3aSGreg Roach                        $workingText       = substr($workingText, $currentLen);
3121821c9e5SGreg Roach                        if ($openParIndex !== false) {
3131821c9e5SGreg Roach                            // Opening parentheses always inherit the following directionality
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                        if (self::$currentState !== '') {
344d5e02c3aSGreg Roach                            $result            .= self::$waitingText;
345d5e02c3aSGreg Roach                            self::$waitingText = '';
346d5e02c3aSGreg Roach                        }
347d5e02c3aSGreg Roach                        break 2; // double break because we're waiting for more information
348d5e02c3aSGreg Roach                    }
349d5e02c3aSGreg Roach                    if ($newState !== self::$currentState) {
350d5e02c3aSGreg Roach                        // A direction change has occurred
351d5e02c3aSGreg Roach                        self::finishCurrentSpan($result);
352d5e02c3aSGreg Roach                        self::$previousState = self::$currentState;
353d5e02c3aSGreg Roach                        self::$currentState  = $newState;
354d5e02c3aSGreg Roach                        self::beginCurrentSpan($result);
355d5e02c3aSGreg Roach                    }
356d5e02c3aSGreg Roach                    self::$waitingText .= $currentLetter;
357d5e02c3aSGreg Roach                    $workingText       = substr($workingText, $currentLen);
358d5e02c3aSGreg Roach                    $result            .= self::$waitingText;
359d5e02c3aSGreg Roach                    self::$waitingText = '';
360d5e02c3aSGreg Roach
361d5e02c3aSGreg Roach                    foreach ($openParDirection as $index => $value) {
362d5e02c3aSGreg Roach                        // Since we now know the proper direction, remember it for all waiting opening parentheses
363d5e02c3aSGreg Roach                        if ($value === '?') {
364d5e02c3aSGreg Roach                            $openParDirection[$index] = self::$currentState;
365d5e02c3aSGreg Roach                        }
366d5e02c3aSGreg Roach                    }
367d5e02c3aSGreg Roach
368d5e02c3aSGreg Roach                    break;
369d5e02c3aSGreg Roach            }
370d5e02c3aSGreg Roach        }
371d5e02c3aSGreg Roach
372d5e02c3aSGreg Roach        // We're done. Finish last <span> if necessary
373d5e02c3aSGreg Roach        if ($numberState) {
374d5e02c3aSGreg Roach            if (self::$waitingText === '') {
375d5e02c3aSGreg Roach                if (self::$currentState === 'RTL') {
376d5e02c3aSGreg Roach                    $result .= self::UTF8_PDF;
377d5e02c3aSGreg Roach                }
3786dcdd572SGreg Roach            } elseif (self::$currentState === 'RTL') {
379d5e02c3aSGreg Roach                self::$waitingText .= self::UTF8_PDF;
380d5e02c3aSGreg Roach            }
381d5e02c3aSGreg Roach        }
382d5e02c3aSGreg Roach        self::finishCurrentSpan($result, true);
383d5e02c3aSGreg Roach
384d5e02c3aSGreg Roach        // Get rid of any waiting text
385d5e02c3aSGreg Roach        if (self::$waitingText !== '') {
386d5e02c3aSGreg Roach            if (I18N::direction() === 'rtl' && self::$currentState === 'LTR') {
387d5e02c3aSGreg Roach                $result .= self::START_RTL;
388d5e02c3aSGreg Roach                $result .= self::$waitingText;
389d5e02c3aSGreg Roach                $result .= self::END_RTL;
390d5e02c3aSGreg Roach            } else {
391d5e02c3aSGreg Roach                $result .= self::START_LTR;
392d5e02c3aSGreg Roach                $result .= self::$waitingText;
393d5e02c3aSGreg Roach                $result .= self::END_LTR;
394d5e02c3aSGreg Roach            }
395d5e02c3aSGreg Roach            self::$waitingText = '';
396d5e02c3aSGreg Roach        }
397d5e02c3aSGreg Roach
398d5e02c3aSGreg Roach        // Lastly, do some more cleanups
399d5e02c3aSGreg Roach
400d5e02c3aSGreg Roach        // Move leading RTL numeric strings to following LTR text
401d5e02c3aSGreg Roach        // (this happens when the page direction is RTL and the original text begins with a number and is followed by LTR text)
402d5e02c3aSGreg Roach        while (substr($result, 0, self::LENGTH_START + 3) === self::START_RTL . self::UTF8_LRE) {
403d5e02c3aSGreg Roach            $spanEnd = strpos($result, self::END_RTL . self::START_LTR);
404d5e02c3aSGreg Roach            if ($spanEnd === false) {
405d5e02c3aSGreg Roach                break;
406d5e02c3aSGreg Roach            }
407d5e02c3aSGreg Roach            $textSpan = self::stripLrmRlm(substr($result, self::LENGTH_START + 3, $spanEnd - self::LENGTH_START - 3));
408d5e02c3aSGreg Roach            if (I18N::scriptDirection(I18N::textScript($textSpan)) === 'rtl') {
409d5e02c3aSGreg Roach                break;
410d5e02c3aSGreg Roach            }
411d5e02c3aSGreg Roach            $result = self::START_LTR . substr($result, self::LENGTH_START, $spanEnd - self::LENGTH_START) . substr($result, $spanEnd + self::LENGTH_START + self::LENGTH_END);
412d5e02c3aSGreg Roach            break;
413d5e02c3aSGreg Roach        }
414d5e02c3aSGreg Roach
415d5e02c3aSGreg Roach        // On RTL pages, put trailing "." in RTL numeric strings into its own RTL span
416d5e02c3aSGreg Roach        if (I18N::direction() === 'rtl') {
417d5e02c3aSGreg Roach            $result = str_replace(self::UTF8_PDF . '.' . self::END_RTL, self::UTF8_PDF . self::END_RTL . self::START_RTL . '.' . self::END_RTL, $result);
418d5e02c3aSGreg Roach        }
419d5e02c3aSGreg Roach
420d5e02c3aSGreg Roach        // Trim trailing blanks preceding <br> in LTR text
421d5e02c3aSGreg Roach        while (self::$previousState !== 'RTL') {
422d5e02c3aSGreg Roach            if (str_contains($result, ' <LTRbr>')) {
423d5e02c3aSGreg Roach                $result = str_replace(' <LTRbr>', '<LTRbr>', $result);
424d5e02c3aSGreg Roach                continue;
425d5e02c3aSGreg Roach            }
426d5e02c3aSGreg Roach            if (str_contains($result, '&nbsp;<LTRbr>')) {
427d5e02c3aSGreg Roach                $result = str_replace('&nbsp;<LTRbr>', '<LTRbr>', $result);
428d5e02c3aSGreg Roach                continue;
429d5e02c3aSGreg Roach            }
430d5e02c3aSGreg Roach            if (str_contains($result, ' <br>')) {
431d5e02c3aSGreg Roach                $result = str_replace(' <br>', '<br>', $result);
432d5e02c3aSGreg Roach                continue;
433d5e02c3aSGreg Roach            }
434d5e02c3aSGreg Roach            if (str_contains($result, '&nbsp;<br>')) {
435d5e02c3aSGreg Roach                $result = str_replace('&nbsp;<br>', '<br>', $result);
436d5e02c3aSGreg Roach                continue;
437d5e02c3aSGreg Roach            }
438d5e02c3aSGreg Roach            break; // Neither space nor &nbsp; : we're done
439d5e02c3aSGreg Roach        }
440d5e02c3aSGreg Roach
441d5e02c3aSGreg Roach        // Trim trailing blanks preceding <br> in RTL text
442d5e02c3aSGreg Roach        while (true) {
443d5e02c3aSGreg Roach            if (str_contains($result, ' <RTLbr>')) {
444d5e02c3aSGreg Roach                $result = str_replace(' <RTLbr>', '<RTLbr>', $result);
445d5e02c3aSGreg Roach                continue;
446d5e02c3aSGreg Roach            }
447d5e02c3aSGreg Roach            if (str_contains($result, '&nbsp;<RTLbr>')) {
448d5e02c3aSGreg Roach                $result = str_replace('&nbsp;<RTLbr>', '<RTLbr>', $result);
449d5e02c3aSGreg Roach                continue;
450d5e02c3aSGreg Roach            }
451d5e02c3aSGreg Roach            break; // Neither space nor &nbsp; : we're done
452d5e02c3aSGreg Roach        }
453d5e02c3aSGreg Roach
454d5e02c3aSGreg Roach        // Convert '<LTRbr>' and '<RTLbr'
455d5e02c3aSGreg Roach        $result = str_replace([
456d5e02c3aSGreg Roach            '<LTRbr>',
457d5e02c3aSGreg Roach            '<RTLbr>',
458d5e02c3aSGreg Roach        ], [
459d5e02c3aSGreg Roach            self::END_LTR . '<br>' . self::START_LTR,
460d5e02c3aSGreg Roach            self::END_RTL . '<br>' . self::START_RTL,
461d5e02c3aSGreg Roach        ], $result);
462d5e02c3aSGreg Roach
463d5e02c3aSGreg Roach        // Include leading indeterminate directional text in whatever follows
464c5b48766SGreg 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>')) {
465d5e02c3aSGreg Roach            $leadingText = '';
466d5e02c3aSGreg Roach            while (true) {
467d5e02c3aSGreg Roach                if ($result === '') {
468d5e02c3aSGreg Roach                    $result = $leadingText;
469d5e02c3aSGreg Roach                    break;
470d5e02c3aSGreg Roach                }
471d5e02c3aSGreg Roach                if (substr($result . "\n", 0, self::LENGTH_START) !== self::START_LTR && substr($result . "\n", 0, self::LENGTH_START) !== self::START_RTL) {
472d5e02c3aSGreg Roach                    $leadingText .= substr($result, 0, 1);
473d5e02c3aSGreg Roach                    $result      = substr($result, 1);
474d5e02c3aSGreg Roach                    continue;
475d5e02c3aSGreg Roach                }
476d5e02c3aSGreg Roach                $result = substr($result, 0, self::LENGTH_START) . $leadingText . substr($result, self::LENGTH_START);
477d5e02c3aSGreg Roach                break;
478d5e02c3aSGreg Roach            }
479d5e02c3aSGreg Roach        }
480d5e02c3aSGreg Roach
481d5e02c3aSGreg Roach        // Include solitary "-" and "+" in surrounding RTL text
482d5e02c3aSGreg Roach        $result = str_replace([
483d5e02c3aSGreg Roach            self::END_RTL . self::START_LTR . '-' . self::END_LTR . self::START_RTL,
484d5e02c3aSGreg Roach            self::END_RTL . self::START_LTR . '+' . self::END_LTR . self::START_RTL,
485d5e02c3aSGreg Roach        ], [
486d5e02c3aSGreg Roach            '-',
487d5e02c3aSGreg Roach            '+',
488d5e02c3aSGreg Roach        ], $result);
489d5e02c3aSGreg Roach
490d5e02c3aSGreg Roach        //$result = strtr($result, [
491d5e02c3aSGreg Roach        //    self::END_RTL . self::START_LTR . '-' . self::END_LTR . self::START_RTL => '-',
492d5e02c3aSGreg Roach        //    self::END_RTL . self::START_LTR . '+' . self::END_LTR . self::START_RTL => '+',
493d5e02c3aSGreg Roach        //]);
494d5e02c3aSGreg Roach
495d5e02c3aSGreg Roach        // Remove empty spans
496d5e02c3aSGreg Roach        $result = str_replace([
497d5e02c3aSGreg Roach            self::START_LTR . self::END_LTR,
498d5e02c3aSGreg Roach            self::START_RTL . self::END_RTL,
499d5e02c3aSGreg Roach        ], '', $result);
500d5e02c3aSGreg Roach
501d5e02c3aSGreg Roach        // Finally, correct '<LTR>', '</LTR>', '<RTL>', and '</RTL>'
502d5e02c3aSGreg Roach        // LTR text: <span dir="ltr"> text </span>
503d5e02c3aSGreg Roach        // RTL text: <span dir="rtl"> text </span>
504d5e02c3aSGreg Roach
505d5e02c3aSGreg Roach        $result = str_replace([
506d5e02c3aSGreg Roach            self::START_LTR,
507d5e02c3aSGreg Roach            self::END_LTR,
508d5e02c3aSGreg Roach            self::START_RTL,
509d5e02c3aSGreg Roach            self::END_RTL,
510d5e02c3aSGreg Roach        ], [
511d5e02c3aSGreg Roach            '<span dir="ltr">',
512d5e02c3aSGreg Roach            '</span>',
513d5e02c3aSGreg Roach            '<span dir="rtl">',
514d5e02c3aSGreg Roach            '</span>',
515d5e02c3aSGreg Roach        ], $result);
516d5e02c3aSGreg Roach
517d5e02c3aSGreg Roach        return $result;
518d5e02c3aSGreg Roach    }
519d5e02c3aSGreg Roach
520d5e02c3aSGreg Roach    /**
521d5e02c3aSGreg Roach     * Wrap words that have an asterisk suffix in <u> and </u> tags.
522d5e02c3aSGreg Roach     * This should underline starred names to show the preferred name.
523d5e02c3aSGreg Roach     *
524d5e02c3aSGreg Roach     * @param string $textSpan
525d5e02c3aSGreg Roach     * @param string $direction
526d5e02c3aSGreg Roach     *
527d5e02c3aSGreg Roach     * @return string
528d5e02c3aSGreg Roach     */
529d5e02c3aSGreg Roach    private static function starredName(string $textSpan, string $direction): string
530d5e02c3aSGreg Roach    {
531d5e02c3aSGreg Roach        // To avoid a TCPDF bug that mixes up the word order, insert those <u> and </u> tags
532d5e02c3aSGreg Roach        // only when page and span directions are identical.
533d5e02c3aSGreg Roach        if ($direction === strtoupper(I18N::direction())) {
534d5e02c3aSGreg Roach            while (true) {
535d5e02c3aSGreg Roach                $starPos = strpos($textSpan, '*');
536d5e02c3aSGreg Roach                if ($starPos === false) {
537d5e02c3aSGreg Roach                    break;
538d5e02c3aSGreg Roach                }
539d5e02c3aSGreg Roach                $trailingText = substr($textSpan, $starPos + 1);
540d5e02c3aSGreg Roach                $textSpan     = substr($textSpan, 0, $starPos);
541d5e02c3aSGreg Roach                $wordStart    = strrpos($textSpan, ' '); // Find the start of the word
542d5e02c3aSGreg Roach                if ($wordStart !== false) {
543d5e02c3aSGreg Roach                    $leadingText = substr($textSpan, 0, $wordStart + 1);
544d5e02c3aSGreg Roach                    $wordText    = substr($textSpan, $wordStart + 1);
545d5e02c3aSGreg Roach                } else {
546d5e02c3aSGreg Roach                    $leadingText = '';
547d5e02c3aSGreg Roach                    $wordText    = $textSpan;
548d5e02c3aSGreg Roach                }
549d5e02c3aSGreg Roach                $textSpan = $leadingText . '<u>' . $wordText . '</u>' . $trailingText;
550d5e02c3aSGreg Roach            }
551d5e02c3aSGreg Roach            $textSpan = preg_replace('~<span class="starredname">(.*)</span>~', '<u>\1</u>', $textSpan);
552d5e02c3aSGreg Roach            // The &nbsp; is a work-around for a TCPDF bug eating blanks.
553d5e02c3aSGreg Roach            $textSpan = str_replace([
554d5e02c3aSGreg Roach                ' <u>',
555d5e02c3aSGreg Roach                '</u> ',
556d5e02c3aSGreg Roach            ], [
557d5e02c3aSGreg Roach                '&nbsp;<u>',
558d5e02c3aSGreg Roach                '</u>&nbsp;',
559d5e02c3aSGreg Roach            ], $textSpan);
560d5e02c3aSGreg Roach        } else {
561d5e02c3aSGreg Roach            // Text and page directions differ:  remove the <span> and </span>
562d5e02c3aSGreg Roach            $textSpan = preg_replace('~(.*)\*~', '\1', $textSpan);
563d5e02c3aSGreg Roach            $textSpan = preg_replace('~<span class="starredname">(.*)</span>~', '\1', $textSpan);
564d5e02c3aSGreg Roach        }
565d5e02c3aSGreg Roach
566d5e02c3aSGreg Roach        return $textSpan;
567d5e02c3aSGreg Roach    }
568d5e02c3aSGreg Roach
569d5e02c3aSGreg Roach    /**
570d5e02c3aSGreg Roach     * Get the next character from an input string
571d5e02c3aSGreg Roach     *
572d5e02c3aSGreg Roach     * @param string $text
573d5e02c3aSGreg Roach     * @param int    $offset
574d5e02c3aSGreg Roach     *
575bd29d468SGreg Roach     * @return array{letter:string,length:int}
576d5e02c3aSGreg Roach     */
577d5e02c3aSGreg Roach    private static function getChar(string $text, int $offset): array
578d5e02c3aSGreg Roach    {
579d5e02c3aSGreg Roach        if ($text === '') {
580d5e02c3aSGreg Roach            return [
581d5e02c3aSGreg Roach                'letter' => '',
582d5e02c3aSGreg Roach                'length' => 0,
583d5e02c3aSGreg Roach            ];
584d5e02c3aSGreg Roach        }
585d5e02c3aSGreg Roach
586d5e02c3aSGreg Roach        $char   = substr($text, $offset, 1);
587d5e02c3aSGreg Roach        $length = 1;
588d5e02c3aSGreg Roach        if ((ord($char) & 0xE0) === 0xC0) {
589d5e02c3aSGreg Roach            $length = 2;
590d5e02c3aSGreg Roach        }
591d5e02c3aSGreg Roach        if ((ord($char) & 0xF0) === 0xE0) {
592d5e02c3aSGreg Roach            $length = 3;
593d5e02c3aSGreg Roach        }
594d5e02c3aSGreg Roach        if ((ord($char) & 0xF8) === 0xF0) {
595d5e02c3aSGreg Roach            $length = 4;
596d5e02c3aSGreg Roach        }
597d5e02c3aSGreg Roach        $letter = substr($text, $offset, $length);
598d5e02c3aSGreg Roach
599d5e02c3aSGreg Roach        return [
600d5e02c3aSGreg Roach            'letter' => $letter,
601d5e02c3aSGreg Roach            'length' => $length,
602d5e02c3aSGreg Roach        ];
603d5e02c3aSGreg Roach    }
604d5e02c3aSGreg Roach
605d5e02c3aSGreg Roach    /**
606d5e02c3aSGreg Roach     * Insert <br> into current span
607d5e02c3aSGreg Roach     *
608d5e02c3aSGreg Roach     * @param string $result
609d5e02c3aSGreg Roach     *
610d5e02c3aSGreg Roach     * @return void
611d5e02c3aSGreg Roach     */
612d5e02c3aSGreg Roach    private static function breakCurrentSpan(string &$result): void
613d5e02c3aSGreg Roach    {
614d5e02c3aSGreg Roach        // Interrupt the current span, insert that <br>, and then continue the current span
615d5e02c3aSGreg Roach        $result            .= self::$waitingText;
616d5e02c3aSGreg Roach        self::$waitingText = '';
617d5e02c3aSGreg Roach
618d5e02c3aSGreg Roach        $breakString = '<' . self::$currentState . 'br>';
619d5e02c3aSGreg Roach        $result      .= $breakString;
620d5e02c3aSGreg Roach    }
621d5e02c3aSGreg Roach
622d5e02c3aSGreg Roach    /**
623d5e02c3aSGreg Roach     * Begin current span
624d5e02c3aSGreg Roach     *
625d5e02c3aSGreg Roach     * @param string $result
626d5e02c3aSGreg Roach     *
627d5e02c3aSGreg Roach     * @return void
628d5e02c3aSGreg Roach     */
629d5e02c3aSGreg Roach    private static function beginCurrentSpan(string &$result): void
630d5e02c3aSGreg Roach    {
631d5e02c3aSGreg Roach        if (self::$currentState === 'LTR') {
632d5e02c3aSGreg Roach            $result .= self::START_LTR;
633d5e02c3aSGreg Roach        }
634d5e02c3aSGreg Roach        if (self::$currentState === 'RTL') {
635d5e02c3aSGreg Roach            $result .= self::START_RTL;
636d5e02c3aSGreg Roach        }
637d5e02c3aSGreg Roach
638d5e02c3aSGreg Roach        self::$posSpanStart = strlen($result);
639d5e02c3aSGreg Roach    }
640d5e02c3aSGreg Roach
641d5e02c3aSGreg Roach    /**
642d5e02c3aSGreg Roach     * Finish current span
643d5e02c3aSGreg Roach     *
644d5e02c3aSGreg Roach     * @param string $result
645d5e02c3aSGreg Roach     * @param bool   $theEnd
646d5e02c3aSGreg Roach     *
647d5e02c3aSGreg Roach     * @return void
648d5e02c3aSGreg Roach     */
649d5e02c3aSGreg Roach    private static function finishCurrentSpan(string &$result, bool $theEnd = false): void
650d5e02c3aSGreg Roach    {
651d5e02c3aSGreg Roach        $textSpan = substr($result, self::$posSpanStart);
652d5e02c3aSGreg Roach        $result   = substr($result, 0, self::$posSpanStart);
653d5e02c3aSGreg Roach
654d5e02c3aSGreg Roach        // Get rid of empty spans, so that our check for presence of RTL will work
655d5e02c3aSGreg Roach        $result = str_replace([
656d5e02c3aSGreg Roach            self::START_LTR . self::END_LTR,
657d5e02c3aSGreg Roach            self::START_RTL . self::END_RTL,
658d5e02c3aSGreg Roach        ], '', $result);
659d5e02c3aSGreg Roach
660d5e02c3aSGreg Roach        // Look for numeric strings that are times (hh:mm:ss). These have to be separated from surrounding numbers.
661d5e02c3aSGreg Roach        $tempResult = '';
662d5e02c3aSGreg Roach        while ($textSpan !== '') {
663d5e02c3aSGreg Roach            $posColon = strpos($textSpan, ':');
664d5e02c3aSGreg Roach            if ($posColon === false) {
665d5e02c3aSGreg Roach                break;
666d5e02c3aSGreg Roach            } // No more possible time strings
667d5e02c3aSGreg Roach            $posLRE = strpos($textSpan, self::UTF8_LRE);
668d5e02c3aSGreg Roach            if ($posLRE === false) {
669d5e02c3aSGreg Roach                break;
670d5e02c3aSGreg Roach            } // No more numeric strings
671d5e02c3aSGreg Roach            $posPDF = strpos($textSpan, self::UTF8_PDF, $posLRE);
672d5e02c3aSGreg Roach            if ($posPDF === false) {
673d5e02c3aSGreg Roach                break;
674d5e02c3aSGreg Roach            } // No more numeric strings
675d5e02c3aSGreg Roach
676d5e02c3aSGreg Roach            $tempResult    .= substr($textSpan, 0, $posLRE + 3); // Copy everything preceding the numeric string
677d5e02c3aSGreg Roach            $numericString = substr($textSpan, $posLRE + 3, $posPDF - $posLRE); // Separate the entire numeric string
678d5e02c3aSGreg Roach            $textSpan      = substr($textSpan, $posPDF + 3);
679d5e02c3aSGreg Roach            $posColon      = strpos($numericString, ':');
680d5e02c3aSGreg Roach            if ($posColon === false) {
681d5e02c3aSGreg Roach                // Nothing that looks like a time here
682d5e02c3aSGreg Roach                $tempResult .= $numericString;
683d5e02c3aSGreg Roach                continue;
684d5e02c3aSGreg Roach            }
685d5e02c3aSGreg Roach            $posBlank = strpos($numericString . ' ', ' ');
686d5e02c3aSGreg Roach            $posNbsp  = strpos($numericString . '&nbsp;', '&nbsp;');
687d5e02c3aSGreg Roach            if ($posBlank < $posNbsp) {
688d5e02c3aSGreg Roach                $posSeparator    = $posBlank;
689d5e02c3aSGreg Roach                $lengthSeparator = 1;
690d5e02c3aSGreg Roach            } else {
691d5e02c3aSGreg Roach                $posSeparator    = $posNbsp;
692d5e02c3aSGreg Roach                $lengthSeparator = 6;
693d5e02c3aSGreg Roach            }
694d5e02c3aSGreg Roach            if ($posColon > $posSeparator) {
695d5e02c3aSGreg Roach                // We have a time string preceded by a blank: Exclude that blank from the numeric string
696d5e02c3aSGreg Roach                $tempResult    .= substr($numericString, 0, $posSeparator);
697d5e02c3aSGreg Roach                $tempResult    .= self::UTF8_PDF;
698d5e02c3aSGreg Roach                $tempResult    .= substr($numericString, $posSeparator, $lengthSeparator);
699d5e02c3aSGreg Roach                $tempResult    .= self::UTF8_LRE;
700d5e02c3aSGreg Roach                $numericString = substr($numericString, $posSeparator + $lengthSeparator);
701d5e02c3aSGreg Roach            }
702d5e02c3aSGreg Roach
703d5e02c3aSGreg Roach            $posBlank = strpos($numericString, ' ');
704d5e02c3aSGreg Roach            $posNbsp  = strpos($numericString, '&nbsp;');
705d5e02c3aSGreg Roach            if ($posBlank === false && $posNbsp === false) {
706d5e02c3aSGreg Roach                // The time string isn't followed by a blank
707d5e02c3aSGreg Roach                $textSpan = $numericString . $textSpan;
708d5e02c3aSGreg Roach                continue;
709d5e02c3aSGreg Roach            }
710d5e02c3aSGreg Roach
711d5e02c3aSGreg Roach            // We have a time string followed by a blank: Exclude that blank from the numeric string
712d5e02c3aSGreg Roach            if ($posBlank === false) {
713d5e02c3aSGreg Roach                $posSeparator    = $posNbsp;
714d5e02c3aSGreg Roach                $lengthSeparator = 6;
715d5e02c3aSGreg Roach            } elseif ($posNbsp === false) {
716d5e02c3aSGreg Roach                $posSeparator    = $posBlank;
717d5e02c3aSGreg Roach                $lengthSeparator = 1;
718d5e02c3aSGreg Roach            } elseif ($posBlank < $posNbsp) {
719d5e02c3aSGreg Roach                $posSeparator    = $posBlank;
720d5e02c3aSGreg Roach                $lengthSeparator = 1;
721d5e02c3aSGreg Roach            } else {
722d5e02c3aSGreg Roach                $posSeparator    = $posNbsp;
723d5e02c3aSGreg Roach                $lengthSeparator = 6;
724d5e02c3aSGreg Roach            }
725d5e02c3aSGreg Roach            $tempResult    .= substr($numericString, 0, $posSeparator);
726d5e02c3aSGreg Roach            $tempResult    .= self::UTF8_PDF;
727d5e02c3aSGreg Roach            $tempResult    .= substr($numericString, $posSeparator, $lengthSeparator);
728d5e02c3aSGreg Roach            $posSeparator  += $lengthSeparator;
729d5e02c3aSGreg Roach            $numericString = substr($numericString, $posSeparator);
730d5e02c3aSGreg Roach            $textSpan      = self::UTF8_LRE . $numericString . $textSpan;
731d5e02c3aSGreg Roach        }
732d5e02c3aSGreg Roach        $textSpan       = $tempResult . $textSpan;
733d5e02c3aSGreg Roach        $trailingBlanks = '';
734d5e02c3aSGreg Roach        $trailingBreaks = '';
735d5e02c3aSGreg Roach
736d5e02c3aSGreg Roach        /* ****************************** LTR text handling ******************************** */
737d5e02c3aSGreg Roach
738d5e02c3aSGreg Roach        if (self::$currentState === 'LTR') {
739d5e02c3aSGreg Roach            // Move trailing numeric strings to the following RTL text. Include any blanks preceding or following the numeric text too.
740d5e02c3aSGreg Roach            if (I18N::direction() === 'rtl' && self::$previousState === 'RTL' && !$theEnd) {
741d5e02c3aSGreg Roach                $trailingString = '';
742d5e02c3aSGreg Roach                $savedSpan      = $textSpan;
743d5e02c3aSGreg Roach                while ($textSpan !== '') {
744d5e02c3aSGreg Roach                    // Look for trailing spaces and tentatively move them
745c5b48766SGreg Roach                    if (str_ends_with($textSpan, ' ')) {
746d5e02c3aSGreg Roach                        $trailingString = ' ' . $trailingString;
747d5e02c3aSGreg Roach                        $textSpan       = substr($textSpan, 0, -1);
748d5e02c3aSGreg Roach                        continue;
749d5e02c3aSGreg Roach                    }
750c5b48766SGreg Roach                    if (str_ends_with($textSpan, '&nbsp;')) {
751d5e02c3aSGreg Roach                        $trailingString = '&nbsp;' . $trailingString;
752d5e02c3aSGreg Roach                        $textSpan       = substr($textSpan, 0, -1);
753d5e02c3aSGreg Roach                        continue;
754d5e02c3aSGreg Roach                    }
755d5e02c3aSGreg Roach                    if (substr($textSpan, -3) !== self::UTF8_PDF) {
756d5e02c3aSGreg Roach                        // There is no trailing numeric string
757d5e02c3aSGreg Roach                        $textSpan = $savedSpan;
758d5e02c3aSGreg Roach                        break;
759d5e02c3aSGreg Roach                    }
760d5e02c3aSGreg Roach
761d5e02c3aSGreg Roach                    // We have a numeric string
762d5e02c3aSGreg Roach                    $posStartNumber = strrpos($textSpan, self::UTF8_LRE);
763d5e02c3aSGreg Roach                    if ($posStartNumber === false) {
764d5e02c3aSGreg Roach                        $posStartNumber = 0;
765d5e02c3aSGreg Roach                    }
766d5e02c3aSGreg Roach                    $trailingString = substr($textSpan, $posStartNumber) . $trailingString;
767d5e02c3aSGreg Roach                    $textSpan       = substr($textSpan, 0, $posStartNumber);
768d5e02c3aSGreg Roach
769d5e02c3aSGreg Roach                    // Look for more spaces and move them too
770d5e02c3aSGreg Roach                    while ($textSpan !== '') {
771c5b48766SGreg Roach                        if (str_ends_with($textSpan, ' ')) {
772d5e02c3aSGreg Roach                            $trailingString = ' ' . $trailingString;
773d5e02c3aSGreg Roach                            $textSpan       = substr($textSpan, 0, -1);
774d5e02c3aSGreg Roach                            continue;
775d5e02c3aSGreg Roach                        }
776c5b48766SGreg Roach                        if (str_ends_with($textSpan, '&nbsp;')) {
777d5e02c3aSGreg Roach                            $trailingString = '&nbsp;' . $trailingString;
778d5e02c3aSGreg Roach                            $textSpan       = substr($textSpan, 0, -1);
779d5e02c3aSGreg Roach                            continue;
780d5e02c3aSGreg Roach                        }
781d5e02c3aSGreg Roach                        break;
782d5e02c3aSGreg Roach                    }
783d5e02c3aSGreg Roach
784d5e02c3aSGreg Roach                    self::$waitingText = $trailingString . self::$waitingText;
785d5e02c3aSGreg Roach                    break;
786d5e02c3aSGreg Roach                }
787d5e02c3aSGreg Roach            }
788d5e02c3aSGreg Roach
789d5e02c3aSGreg Roach            $savedSpan = $textSpan;
790d5e02c3aSGreg Roach            // Move any trailing <br>, optionally preceded or followed by blanks, outside this LTR span
791d5e02c3aSGreg Roach            while ($textSpan !== '') {
792c5b48766SGreg Roach                if (str_ends_with($textSpan, ' ')) {
793d5e02c3aSGreg Roach                    $trailingBlanks = ' ' . $trailingBlanks;
794d5e02c3aSGreg Roach                    $textSpan       = substr($textSpan, 0, -1);
795d5e02c3aSGreg Roach                    continue;
796d5e02c3aSGreg Roach                }
797c5b48766SGreg Roach                if (str_ends_with('......' . $textSpan, '&nbsp;')) {
798d5e02c3aSGreg Roach                    $trailingBlanks = '&nbsp;' . $trailingBlanks;
799d5e02c3aSGreg Roach                    $textSpan       = substr($textSpan, 0, -6);
800d5e02c3aSGreg Roach                    continue;
801d5e02c3aSGreg Roach                }
802d5e02c3aSGreg Roach                break;
803d5e02c3aSGreg Roach            }
804c5b48766SGreg Roach            while (str_ends_with($textSpan, '<LTRbr>')) {
805d5e02c3aSGreg Roach                $trailingBreaks = '<br>' . $trailingBreaks; // Plain <br> because it’s outside a span
806d5e02c3aSGreg Roach                $textSpan       = substr($textSpan, 0, -7);
807d5e02c3aSGreg Roach            }
808d5e02c3aSGreg Roach            if ($trailingBreaks !== '') {
809d5e02c3aSGreg Roach                while ($textSpan !== '') {
810c5b48766SGreg Roach                    if (str_ends_with($textSpan, ' ')) {
811d5e02c3aSGreg Roach                        $trailingBreaks = ' ' . $trailingBreaks;
812d5e02c3aSGreg Roach                        $textSpan       = substr($textSpan, 0, -1);
813d5e02c3aSGreg Roach                        continue;
814d5e02c3aSGreg Roach                    }
815c5b48766SGreg Roach                    if (str_ends_with($textSpan, '&nbsp;')) {
816d5e02c3aSGreg Roach                        $trailingBreaks = '&nbsp;' . $trailingBreaks;
817d5e02c3aSGreg Roach                        $textSpan       = substr($textSpan, 0, -6);
818d5e02c3aSGreg Roach                        continue;
819d5e02c3aSGreg Roach                    }
820d5e02c3aSGreg Roach                    break;
821d5e02c3aSGreg Roach                }
822d5e02c3aSGreg Roach                self::$waitingText = $trailingBlanks . self::$waitingText; // Put those trailing blanks inside the following span
823d5e02c3aSGreg Roach            } else {
824d5e02c3aSGreg Roach                $textSpan = $savedSpan;
825d5e02c3aSGreg Roach            }
826d5e02c3aSGreg Roach
827d5e02c3aSGreg Roach            $trailingBlanks      = '';
828d5e02c3aSGreg Roach            $trailingPunctuation = '';
829d5e02c3aSGreg Roach            $trailingID          = '';
830d5e02c3aSGreg Roach            $trailingSeparator   = '';
831d5e02c3aSGreg Roach            $leadingSeparator    = '';
832d5e02c3aSGreg Roach
833d5e02c3aSGreg Roach            while (I18N::direction() === 'rtl') {
834d5e02c3aSGreg Roach                if (str_contains($result, self::START_RTL)) {
835d5e02c3aSGreg Roach                    // Remove trailing blanks for inclusion in a separate LTR span
836d5e02c3aSGreg Roach                    while ($textSpan !== '') {
837c5b48766SGreg Roach                        if (str_ends_with($textSpan, ' ')) {
838d5e02c3aSGreg Roach                            $trailingBlanks = ' ' . $trailingBlanks;
839d5e02c3aSGreg Roach                            $textSpan       = substr($textSpan, 0, -1);
840d5e02c3aSGreg Roach                            continue;
841d5e02c3aSGreg Roach                        }
842c5b48766SGreg Roach                        if (str_ends_with($textSpan, '&nbsp;')) {
843d5e02c3aSGreg Roach                            $trailingBlanks = '&nbsp;' . $trailingBlanks;
844d5e02c3aSGreg Roach                            $textSpan       = substr($textSpan, 0, -1);
845d5e02c3aSGreg Roach                            continue;
846d5e02c3aSGreg Roach                        }
847d5e02c3aSGreg Roach                        break;
848d5e02c3aSGreg Roach                    }
849d5e02c3aSGreg Roach
850d5e02c3aSGreg Roach                    // Remove trailing punctuation for inclusion in a separate LTR span
851d5e02c3aSGreg Roach                    if ($textSpan === '') {
852d5e02c3aSGreg Roach                        $trailingChar = "\n";
853d5e02c3aSGreg Roach                    } else {
854d5e02c3aSGreg Roach                        $trailingChar = substr($textSpan, -1);
855d5e02c3aSGreg Roach                    }
856d5e02c3aSGreg Roach                    if (str_contains(self::PUNCTUATION, $trailingChar)) {
857d5e02c3aSGreg Roach                        $trailingPunctuation = $trailingChar;
858d5e02c3aSGreg Roach                        $textSpan            = substr($textSpan, 0, -1);
859d5e02c3aSGreg Roach                    }
860d5e02c3aSGreg Roach                }
861d5e02c3aSGreg Roach
862d5e02c3aSGreg Roach                // Remove trailing ID numbers that look like "(xnnn)" for inclusion in a separate LTR span
863d5e02c3aSGreg Roach                while (true) {
864c5b48766SGreg Roach                    if (!str_ends_with($textSpan, ')')) {
865d5e02c3aSGreg Roach                        break;
866d5e02c3aSGreg Roach                    } // There is no trailing ')'
867d5e02c3aSGreg Roach                    $posLeftParen = strrpos($textSpan, '(');
868d5e02c3aSGreg Roach                    if ($posLeftParen === false) {
869d5e02c3aSGreg Roach                        break;
870d5e02c3aSGreg Roach                    } // There is no leading '('
871d5e02c3aSGreg Roach                    $temp = self::stripLrmRlm(substr($textSpan, $posLeftParen)); // Get rid of UTF8 control codes
872d5e02c3aSGreg Roach
873d5e02c3aSGreg Roach                    // If the parenthesized text doesn't look like an ID number,
874d5e02c3aSGreg Roach                    // we don't want to touch it.
875d5e02c3aSGreg Roach                    // This check won’t work if somebody uses ID numbers with an unusual format.
876d5e02c3aSGreg Roach                    $offset    = 1;
877d5e02c3aSGreg Roach                    $charArray = self::getChar($temp, $offset); // Get 1st character of parenthesized text
878d5e02c3aSGreg Roach                    if (str_contains(self::NUMBERS, $charArray['letter'])) {
879d5e02c3aSGreg Roach                        break;
880d5e02c3aSGreg Roach                    }
881d5e02c3aSGreg Roach                    $offset += $charArray['length']; // Point at 2nd character of parenthesized text
882d5e02c3aSGreg Roach                    if (!str_contains(self::NUMBERS, substr($temp, $offset, 1))) {
883d5e02c3aSGreg Roach                        break;
884d5e02c3aSGreg Roach                    }
885d5e02c3aSGreg Roach                    // 1st character of parenthesized text is alpha, 2nd character is a digit; last has to be a digit too
886d5e02c3aSGreg Roach                    if (!str_contains(self::NUMBERS, substr($temp, -2, 1))) {
887d5e02c3aSGreg Roach                        break;
888d5e02c3aSGreg Roach                    }
889d5e02c3aSGreg Roach
890d5e02c3aSGreg Roach                    $trailingID = substr($textSpan, $posLeftParen);
891d5e02c3aSGreg Roach                    $textSpan   = substr($textSpan, 0, $posLeftParen);
892d5e02c3aSGreg Roach                    break;
893d5e02c3aSGreg Roach                }
894d5e02c3aSGreg Roach
895d5e02c3aSGreg Roach                // Look for " - " or blank preceding the ID number and remove it for inclusion in a separate LTR span
896d5e02c3aSGreg Roach                if ($trailingID !== '') {
897d5e02c3aSGreg Roach                    while ($textSpan !== '') {
898c5b48766SGreg Roach                        if (str_ends_with($textSpan, ' ')) {
899d5e02c3aSGreg Roach                            $trailingSeparator = ' ' . $trailingSeparator;
900d5e02c3aSGreg Roach                            $textSpan          = substr($textSpan, 0, -1);
901d5e02c3aSGreg Roach                            continue;
902d5e02c3aSGreg Roach                        }
903c5b48766SGreg Roach                        if (str_ends_with($textSpan, '&nbsp;')) {
904d5e02c3aSGreg Roach                            $trailingSeparator = '&nbsp;' . $trailingSeparator;
905d5e02c3aSGreg Roach                            $textSpan          = substr($textSpan, 0, -6);
906d5e02c3aSGreg Roach                            continue;
907d5e02c3aSGreg Roach                        }
908c5b48766SGreg Roach                        if (str_ends_with($textSpan, '-')) {
909d5e02c3aSGreg Roach                            $trailingSeparator = '-' . $trailingSeparator;
910d5e02c3aSGreg Roach                            $textSpan          = substr($textSpan, 0, -1);
911d5e02c3aSGreg Roach                            continue;
912d5e02c3aSGreg Roach                        }
913d5e02c3aSGreg Roach                        break;
914d5e02c3aSGreg Roach                    }
915d5e02c3aSGreg Roach                }
916d5e02c3aSGreg Roach
917d5e02c3aSGreg Roach                // Look for " - " preceding the text and remove it for inclusion in a separate LTR span
918d5e02c3aSGreg Roach                $foundSeparator = false;
919d5e02c3aSGreg Roach                $savedSpan      = $textSpan;
920d5e02c3aSGreg Roach                while ($textSpan !== '') {
921c5b48766SGreg Roach                    if (str_starts_with($textSpan, ' ')) {
922d5e02c3aSGreg Roach                        $leadingSeparator = ' ' . $leadingSeparator;
923d5e02c3aSGreg Roach                        $textSpan         = substr($textSpan, 1);
924d5e02c3aSGreg Roach                        continue;
925d5e02c3aSGreg Roach                    }
926c5b48766SGreg Roach                    if (str_starts_with($textSpan, '&nbsp;')) {
927d5e02c3aSGreg Roach                        $leadingSeparator = '&nbsp;' . $leadingSeparator;
928d5e02c3aSGreg Roach                        $textSpan         = substr($textSpan, 6);
929d5e02c3aSGreg Roach                        continue;
930d5e02c3aSGreg Roach                    }
931c5b48766SGreg Roach                    if (str_starts_with($textSpan, '-')) {
932d5e02c3aSGreg Roach                        $leadingSeparator = '-' . $leadingSeparator;
933d5e02c3aSGreg Roach                        $textSpan         = substr($textSpan, 1);
934d5e02c3aSGreg Roach                        $foundSeparator   = true;
935d5e02c3aSGreg Roach                        continue;
936d5e02c3aSGreg Roach                    }
937d5e02c3aSGreg Roach                    break;
938d5e02c3aSGreg Roach                }
939d5e02c3aSGreg Roach                if (!$foundSeparator) {
940d5e02c3aSGreg Roach                    $textSpan         = $savedSpan;
941d5e02c3aSGreg Roach                    $leadingSeparator = '';
942d5e02c3aSGreg Roach                }
943d5e02c3aSGreg Roach                break;
944d5e02c3aSGreg Roach            }
945d5e02c3aSGreg Roach
946d5e02c3aSGreg Roach            // We're done: finish the span
947d5e02c3aSGreg Roach            $textSpan = self::starredName($textSpan, 'LTR'); // Wrap starred name in <u> and </u> tags
948d5e02c3aSGreg Roach            while (true) {
949d5e02c3aSGreg Roach                // Remove blanks that precede <LTRbr>
950d5e02c3aSGreg Roach                if (str_contains($textSpan, ' <LTRbr>')) {
951d5e02c3aSGreg Roach                    $textSpan = str_replace(' <LTRbr>', '<LTRbr>', $textSpan);
952d5e02c3aSGreg Roach                    continue;
953d5e02c3aSGreg Roach                }
954d5e02c3aSGreg Roach                if (str_contains($textSpan, '&nbsp;<LTRbr>')) {
955d5e02c3aSGreg Roach                    $textSpan = str_replace('&nbsp;<LTRbr>', '<LTRbr>', $textSpan);
956d5e02c3aSGreg Roach                    continue;
957d5e02c3aSGreg Roach                }
958d5e02c3aSGreg Roach                break;
959d5e02c3aSGreg Roach            }
960d5e02c3aSGreg Roach            if ($leadingSeparator !== '') {
961d5e02c3aSGreg Roach                $result .= self::START_LTR . $leadingSeparator . self::END_LTR;
962d5e02c3aSGreg Roach            }
963d5e02c3aSGreg Roach            $result .= $textSpan . self::END_LTR;
964d5e02c3aSGreg Roach            if ($trailingSeparator !== '') {
965d5e02c3aSGreg Roach                $result .= self::START_LTR . $trailingSeparator . self::END_LTR;
966d5e02c3aSGreg Roach            }
967d5e02c3aSGreg Roach            if ($trailingID !== '') {
968d5e02c3aSGreg Roach                $result .= self::START_LTR . $trailingID . self::END_LTR;
969d5e02c3aSGreg Roach            }
970d5e02c3aSGreg Roach            if ($trailingPunctuation !== '') {
971d5e02c3aSGreg Roach                $result .= self::START_LTR . $trailingPunctuation . self::END_LTR;
972d5e02c3aSGreg Roach            }
973d5e02c3aSGreg Roach            if ($trailingBlanks !== '') {
974d5e02c3aSGreg Roach                $result .= self::START_LTR . $trailingBlanks . self::END_LTR;
975d5e02c3aSGreg Roach            }
976d5e02c3aSGreg Roach        }
977d5e02c3aSGreg Roach
978d5e02c3aSGreg Roach        /* ****************************** RTL text handling ******************************** */
979d5e02c3aSGreg Roach
980d5e02c3aSGreg Roach        if (self::$currentState === 'RTL') {
981d5e02c3aSGreg Roach            $savedSpan = $textSpan;
982d5e02c3aSGreg Roach
983d5e02c3aSGreg Roach            // Move any trailing <br>, optionally followed by blanks, outside this RTL span
984d5e02c3aSGreg Roach            while ($textSpan !== '') {
985c5b48766SGreg Roach                if (str_ends_with($textSpan, ' ')) {
986d5e02c3aSGreg Roach                    $trailingBlanks = ' ' . $trailingBlanks;
987d5e02c3aSGreg Roach                    $textSpan       = substr($textSpan, 0, -1);
988d5e02c3aSGreg Roach                    continue;
989d5e02c3aSGreg Roach                }
990c5b48766SGreg Roach                if (str_ends_with('......' . $textSpan, '&nbsp;')) {
991d5e02c3aSGreg Roach                    $trailingBlanks = '&nbsp;' . $trailingBlanks;
992d5e02c3aSGreg Roach                    $textSpan       = substr($textSpan, 0, -6);
993d5e02c3aSGreg Roach                    continue;
994d5e02c3aSGreg Roach                }
995d5e02c3aSGreg Roach                break;
996d5e02c3aSGreg Roach            }
997c5b48766SGreg Roach            while (str_ends_with($textSpan, '<RTLbr>')) {
998d5e02c3aSGreg Roach                $trailingBreaks = '<br>' . $trailingBreaks; // Plain <br> because it’s outside a span
999d5e02c3aSGreg Roach                $textSpan       = substr($textSpan, 0, -7);
1000d5e02c3aSGreg Roach            }
1001d5e02c3aSGreg Roach            if ($trailingBreaks !== '') {
1002d5e02c3aSGreg Roach                self::$waitingText = $trailingBlanks . self::$waitingText; // Put those trailing blanks inside the following span
1003d5e02c3aSGreg Roach            } else {
1004d5e02c3aSGreg Roach                $textSpan = $savedSpan;
1005d5e02c3aSGreg Roach            }
1006d5e02c3aSGreg Roach
1007d5e02c3aSGreg Roach            // Move trailing numeric strings to the following LTR text. Include any blanks preceding or following the numeric text too.
1008d5e02c3aSGreg Roach            if (!$theEnd && I18N::direction() !== 'rtl') {
1009d5e02c3aSGreg Roach                $trailingString = '';
1010d5e02c3aSGreg Roach                $savedSpan      = $textSpan;
1011d5e02c3aSGreg Roach                while ($textSpan !== '') {
1012d5e02c3aSGreg Roach                    // Look for trailing spaces and tentatively move them
1013c5b48766SGreg Roach                    if (str_ends_with($textSpan, ' ')) {
1014d5e02c3aSGreg Roach                        $trailingString = ' ' . $trailingString;
1015d5e02c3aSGreg Roach                        $textSpan       = substr($textSpan, 0, -1);
1016d5e02c3aSGreg Roach                        continue;
1017d5e02c3aSGreg Roach                    }
1018c5b48766SGreg Roach                    if (str_ends_with($textSpan, '&nbsp;')) {
1019d5e02c3aSGreg Roach                        $trailingString = '&nbsp;' . $trailingString;
1020d5e02c3aSGreg Roach                        $textSpan       = substr($textSpan, 0, -1);
1021d5e02c3aSGreg Roach                        continue;
1022d5e02c3aSGreg Roach                    }
1023d5e02c3aSGreg Roach                    if (substr($textSpan, -3) !== self::UTF8_PDF) {
1024d5e02c3aSGreg Roach                        // There is no trailing numeric string
1025d5e02c3aSGreg Roach                        $textSpan = $savedSpan;
1026d5e02c3aSGreg Roach                        break;
1027d5e02c3aSGreg Roach                    }
1028d5e02c3aSGreg Roach
1029d5e02c3aSGreg Roach                    // We have a numeric string
1030d5e02c3aSGreg Roach                    $posStartNumber = strrpos($textSpan, self::UTF8_LRE);
1031d5e02c3aSGreg Roach                    if ($posStartNumber === false) {
1032d5e02c3aSGreg Roach                        $posStartNumber = 0;
1033d5e02c3aSGreg Roach                    }
1034d5e02c3aSGreg Roach                    $trailingString = substr($textSpan, $posStartNumber) . $trailingString;
1035d5e02c3aSGreg Roach                    $textSpan       = substr($textSpan, 0, $posStartNumber);
1036d5e02c3aSGreg Roach
1037d5e02c3aSGreg Roach                    // Look for more spaces and move them too
1038d5e02c3aSGreg Roach                    while ($textSpan !== '') {
1039c5b48766SGreg Roach                        if (str_ends_with($textSpan, ' ')) {
1040d5e02c3aSGreg Roach                            $trailingString = ' ' . $trailingString;
1041d5e02c3aSGreg Roach                            $textSpan       = substr($textSpan, 0, -1);
1042d5e02c3aSGreg Roach                            continue;
1043d5e02c3aSGreg Roach                        }
1044c5b48766SGreg Roach                        if (str_ends_with($textSpan, '&nbsp;')) {
1045d5e02c3aSGreg Roach                            $trailingString = '&nbsp;' . $trailingString;
1046d5e02c3aSGreg Roach                            $textSpan       = substr($textSpan, 0, -1);
1047d5e02c3aSGreg Roach                            continue;
1048d5e02c3aSGreg Roach                        }
1049d5e02c3aSGreg Roach                        break;
1050d5e02c3aSGreg Roach                    }
1051d5e02c3aSGreg Roach
1052d5e02c3aSGreg Roach                    self::$waitingText = $trailingString . self::$waitingText;
1053d5e02c3aSGreg Roach                    break;
1054d5e02c3aSGreg Roach                }
1055d5e02c3aSGreg Roach            }
1056d5e02c3aSGreg Roach
1057d5e02c3aSGreg Roach            // Trailing " - " needs to be prefixed to the following span
1058c5b48766SGreg Roach            if (!$theEnd && str_ends_with('...' . $textSpan, ' - ')) {
1059d5e02c3aSGreg Roach                $textSpan          = substr($textSpan, 0, -3);
1060d5e02c3aSGreg Roach                self::$waitingText = ' - ' . self::$waitingText;
1061d5e02c3aSGreg Roach            }
1062d5e02c3aSGreg Roach
1063d5e02c3aSGreg Roach            while (I18N::direction() === 'rtl') {
1064d5e02c3aSGreg Roach                // Look for " - " preceding <RTLbr> and relocate it to the front of the string
1065d5e02c3aSGreg Roach                $posDashString = strpos($textSpan, ' - <RTLbr>');
1066d5e02c3aSGreg Roach                if ($posDashString === false) {
1067d5e02c3aSGreg Roach                    break;
1068d5e02c3aSGreg Roach                }
1069d5e02c3aSGreg Roach                $posStringStart = strrpos(substr($textSpan, 0, $posDashString), '<RTLbr>');
1070d5e02c3aSGreg Roach                if ($posStringStart === false) {
1071d5e02c3aSGreg Roach                    $posStringStart = 0;
1072d5e02c3aSGreg Roach                } else {
1073d5e02c3aSGreg Roach                    $posStringStart += 9;
1074d5e02c3aSGreg Roach                } // Point to the first char following the last <RTLbr>
1075d5e02c3aSGreg Roach
1076d5e02c3aSGreg Roach                $textSpan = substr($textSpan, 0, $posStringStart) . ' - ' . substr($textSpan, $posStringStart, $posDashString - $posStringStart) . substr($textSpan, $posDashString + 3);
1077d5e02c3aSGreg Roach            }
1078d5e02c3aSGreg Roach
1079d5e02c3aSGreg Roach            // Strip leading spaces from the RTL text
1080d5e02c3aSGreg Roach            $countLeadingSpaces = 0;
1081d5e02c3aSGreg Roach            while ($textSpan !== '') {
1082c5b48766SGreg Roach                if (str_starts_with($textSpan, ' ')) {
1083d5e02c3aSGreg Roach                    $countLeadingSpaces++;
1084d5e02c3aSGreg Roach                    $textSpan = substr($textSpan, 1);
1085d5e02c3aSGreg Roach                    continue;
1086d5e02c3aSGreg Roach                }
1087c5b48766SGreg Roach                if (str_starts_with($textSpan, '&nbsp;')) {
1088d5e02c3aSGreg Roach                    $countLeadingSpaces++;
1089d5e02c3aSGreg Roach                    $textSpan = substr($textSpan, 6);
1090d5e02c3aSGreg Roach                    continue;
1091d5e02c3aSGreg Roach                }
1092d5e02c3aSGreg Roach                break;
1093d5e02c3aSGreg Roach            }
1094d5e02c3aSGreg Roach
1095d5e02c3aSGreg Roach            // Strip trailing spaces from the RTL text
1096d5e02c3aSGreg Roach            $countTrailingSpaces = 0;
1097d5e02c3aSGreg Roach            while ($textSpan !== '') {
1098c5b48766SGreg Roach                if (str_ends_with($textSpan, ' ')) {
1099d5e02c3aSGreg Roach                    $countTrailingSpaces++;
1100d5e02c3aSGreg Roach                    $textSpan = substr($textSpan, 0, -1);
1101d5e02c3aSGreg Roach                    continue;
1102d5e02c3aSGreg Roach                }
1103c5b48766SGreg Roach                if (str_ends_with($textSpan, '&nbsp;')) {
1104d5e02c3aSGreg Roach                    $countTrailingSpaces++;
1105d5e02c3aSGreg Roach                    $textSpan = substr($textSpan, 0, -6);
1106d5e02c3aSGreg Roach                    continue;
1107d5e02c3aSGreg Roach                }
1108d5e02c3aSGreg Roach                break;
1109d5e02c3aSGreg Roach            }
1110d5e02c3aSGreg Roach
1111d5e02c3aSGreg Roach            // Look for trailing " -", reverse it, and relocate it to the front of the string
1112c5b48766SGreg Roach            if (str_ends_with($textSpan, ' -')) {
1113d5e02c3aSGreg Roach                $posDashString  = strlen($textSpan) - 2;
1114d5e02c3aSGreg Roach                $posStringStart = strrpos(substr($textSpan, 0, $posDashString), '<RTLbr>');
1115d5e02c3aSGreg Roach                if ($posStringStart === false) {
1116d5e02c3aSGreg Roach                    $posStringStart = 0;
1117d5e02c3aSGreg Roach                } else {
1118d5e02c3aSGreg Roach                    $posStringStart += 9;
1119d5e02c3aSGreg Roach                } // Point to the first char following the last <RTLbr>
1120d5e02c3aSGreg Roach
1121d5e02c3aSGreg Roach                $textSpan = substr($textSpan, 0, $posStringStart) . '- ' . substr($textSpan, $posStringStart, $posDashString - $posStringStart) . substr($textSpan, $posDashString + 2);
1122d5e02c3aSGreg Roach            }
1123d5e02c3aSGreg Roach
1124d5e02c3aSGreg Roach            if ($countLeadingSpaces !== 0) {
1125d5e02c3aSGreg Roach                $newLength = strlen($textSpan) + $countLeadingSpaces;
1126d5e02c3aSGreg Roach                $textSpan  = str_pad($textSpan, $newLength, ' ', I18N::direction() === 'rtl' ? STR_PAD_LEFT : STR_PAD_RIGHT);
1127d5e02c3aSGreg Roach            }
1128d5e02c3aSGreg Roach            if ($countTrailingSpaces !== 0) {
1129d5e02c3aSGreg Roach                if (I18N::direction() === 'ltr') {
1130d5e02c3aSGreg Roach                    if ($trailingBreaks === '') {
1131d5e02c3aSGreg Roach                        // Move trailing RTL spaces to front of following LTR span
1132d5e02c3aSGreg Roach                        $newLength         = strlen(self::$waitingText) + $countTrailingSpaces;
1133d5e02c3aSGreg Roach                        self::$waitingText = str_pad(self::$waitingText, $newLength, ' ', STR_PAD_LEFT);
1134d5e02c3aSGreg Roach                    }
1135d5e02c3aSGreg Roach                } else {
1136d5e02c3aSGreg Roach                    $newLength = strlen($textSpan) + $countTrailingSpaces;
1137d5e02c3aSGreg Roach                    $textSpan  = str_pad($textSpan, $newLength);
1138d5e02c3aSGreg Roach                }
1139d5e02c3aSGreg Roach            }
1140d5e02c3aSGreg Roach
1141d5e02c3aSGreg Roach            // We're done: finish the span
1142d5e02c3aSGreg Roach            $textSpan = self::starredName($textSpan, 'RTL'); // Wrap starred name in <u> and </u> tags
1143d5e02c3aSGreg Roach            $result   .= $textSpan . self::END_RTL;
1144d5e02c3aSGreg Roach        }
1145d5e02c3aSGreg Roach
1146d5e02c3aSGreg Roach        if (self::$currentState !== 'LTR' && self::$currentState !== 'RTL') {
1147d5e02c3aSGreg Roach            $result .= $textSpan;
1148d5e02c3aSGreg Roach        }
1149d5e02c3aSGreg Roach
1150d5e02c3aSGreg Roach        $result .= $trailingBreaks; // Get rid of any waiting <br>
1151d5e02c3aSGreg Roach    }
1152d5e02c3aSGreg Roach}
1153