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