1d5e02c3aSGreg Roach<?php 2d5e02c3aSGreg Roach 3d5e02c3aSGreg Roach/** 4d5e02c3aSGreg Roach * webtrees: online genealogy 55bfc6897SGreg Roach * Copyright (C) 2022 webtrees development team 6d5e02c3aSGreg Roach * This program is free software: you can redistribute it and/or modify 7d5e02c3aSGreg Roach * it under the terms of the GNU General Public License as published by 8d5e02c3aSGreg Roach * the Free Software Foundation, either version 3 of the License, or 9d5e02c3aSGreg Roach * (at your option) any later version. 10d5e02c3aSGreg Roach * This program is distributed in the hope that it will be useful, 11d5e02c3aSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 12d5e02c3aSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13d5e02c3aSGreg Roach * GNU General Public License for more details. 14d5e02c3aSGreg Roach * You should have received a copy of the GNU General Public License 15d5e02c3aSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 16d5e02c3aSGreg Roach */ 17d5e02c3aSGreg Roach 18d5e02c3aSGreg Roachdeclare(strict_types=1); 19d5e02c3aSGreg Roach 20d5e02c3aSGreg Roachnamespace Fisharebest\Webtrees\Report; 21d5e02c3aSGreg Roach 22d5e02c3aSGreg Roachuse Fisharebest\Webtrees\I18N; 23d5e02c3aSGreg Roach 24005852d3SGreg Roachuse function ord; 25005852d3SGreg Roachuse function preg_replace; 26d5e02c3aSGreg Roachuse function str_contains; 27005852d3SGreg Roachuse function str_pad; 28005852d3SGreg Roachuse function str_replace; 29005852d3SGreg Roachuse function strlen; 30005852d3SGreg Roachuse function strpos; 31005852d3SGreg Roachuse function strrpos; 32005852d3SGreg Roachuse function strtolower; 33005852d3SGreg Roachuse function strtoupper; 34005852d3SGreg Roachuse function substr; 35005852d3SGreg Roach 36005852d3SGreg Roachuse const STR_PAD_LEFT; 37005852d3SGreg Roachuse const STR_PAD_RIGHT; 38d5e02c3aSGreg Roach 39d5e02c3aSGreg Roach/** 40d5e02c3aSGreg Roach * RTL Functions for use in the PDF reports 41d5e02c3aSGreg Roach */ 42d5e02c3aSGreg Roachclass RightToLeftSupport 43d5e02c3aSGreg Roach{ 44d5e02c3aSGreg Roach private const UTF8_LRM = "\xE2\x80\x8E"; // U+200E (Left to Right mark: zero-width character with LTR directionality) 45d5e02c3aSGreg Roach private const UTF8_RLM = "\xE2\x80\x8F"; // U+200F (Right to Left mark: zero-width character with RTL directionality) 46d5e02c3aSGreg Roach private const UTF8_LRO = "\xE2\x80\xAD"; // U+202D (Left to Right override: force everything following to LTR mode) 47d5e02c3aSGreg Roach private const UTF8_RLO = "\xE2\x80\xAE"; // U+202E (Right to Left override: force everything following to RTL mode) 48d5e02c3aSGreg Roach private const UTF8_LRE = "\xE2\x80\xAA"; // U+202A (Left to Right embedding: treat everything following as LTR text) 49d5e02c3aSGreg Roach private const UTF8_RLE = "\xE2\x80\xAB"; // U+202B (Right to Left embedding: treat everything following as RTL text) 50d5e02c3aSGreg Roach private const UTF8_PDF = "\xE2\x80\xAC"; // U+202C (Pop directional formatting: restore state prior to last LRO, RLO, LRE, RLE) 51d5e02c3aSGreg Roach 52d5e02c3aSGreg Roach private const OPEN_PARENTHESES = '([{'; 53d5e02c3aSGreg Roach 54d5e02c3aSGreg Roach private const CLOSE_PARENTHESES = ')]}'; 55d5e02c3aSGreg Roach 56d5e02c3aSGreg Roach private const NUMBERS = '0123456789'; 57d5e02c3aSGreg Roach 58d5e02c3aSGreg Roach private const NUMBER_PREFIX = '+-'; // Treat these like numbers when at beginning or end of numeric strings 59d5e02c3aSGreg Roach 60d5e02c3aSGreg Roach private const NUMBER_PUNCTUATION = '- ,.:/'; // Treat these like numbers when inside numeric strings 61d5e02c3aSGreg Roach 62d5e02c3aSGreg Roach private const PUNCTUATION = ',.:;?!'; 63d5e02c3aSGreg Roach 64d5e02c3aSGreg Roach // Markup 65d5e02c3aSGreg Roach private const START_LTR = '<LTR>'; 66d5e02c3aSGreg Roach private const END_LTR = '</LTR>'; 67d5e02c3aSGreg Roach private const START_RTL = '<RTL>'; 68d5e02c3aSGreg Roach private const END_RTL = '</RTL>'; 69d5e02c3aSGreg Roach private const LENGTH_START = 5; 70d5e02c3aSGreg Roach private const LENGTH_END = 6; 71d5e02c3aSGreg Roach 727fa97a69SGreg Roach /* Were we previously processing LTR or RTL. */ 737fa97a69SGreg Roach private static string $previousState; 74d5e02c3aSGreg Roach 757fa97a69SGreg Roach /* Are we currently processing LTR or RTL. */ 767fa97a69SGreg Roach private static string $currentState; 77d5e02c3aSGreg Roach 787fa97a69SGreg Roach /* Text waiting to be processed. */ 797fa97a69SGreg Roach private static string $waitingText; 80d5e02c3aSGreg Roach 817fa97a69SGreg Roach /* Offset into the text. */ 827fa97a69SGreg Roach private static int $posSpanStart; 83d5e02c3aSGreg Roach 84d5e02c3aSGreg Roach /** 85d5e02c3aSGreg Roach * This function strips ‎ and ‏ 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 ‎ and ‏ characters should be stripped 90d5e02c3aSGreg Roach * 91d5e02c3aSGreg Roach * @return string The input string, with ‎ and ‏ 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 '‎', 104d5e02c3aSGreg Roach '‏', 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) === ' ') { 183d5e02c3aSGreg Roach $entity = ' '; // 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) === ' ') { 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 === ' ') { 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); 312*1821c9e5SGreg Roach if ($openParIndex !== false) { 313*1821c9e5SGreg 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, ' ')) { 325d5e02c3aSGreg Roach // Spaces following this left parenthesis inherit the following directionality too 326d5e02c3aSGreg Roach self::$waitingText .= ' '; 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, ' <LTRbr>')) { 427d5e02c3aSGreg Roach $result = str_replace(' <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, ' <br>')) { 435d5e02c3aSGreg Roach $result = str_replace(' <br>', '<br>', $result); 436d5e02c3aSGreg Roach continue; 437d5e02c3aSGreg Roach } 438d5e02c3aSGreg Roach break; // Neither space nor : 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, ' <RTLbr>')) { 448d5e02c3aSGreg Roach $result = str_replace(' <RTLbr>', '<RTLbr>', $result); 449d5e02c3aSGreg Roach continue; 450d5e02c3aSGreg Roach } 451d5e02c3aSGreg Roach break; // Neither space nor : 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 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 ' <u>', 558d5e02c3aSGreg Roach '</u> ', 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 . ' ', ' '); 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, ' '); 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, ' ')) { 751d5e02c3aSGreg Roach $trailingString = ' ' . $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, ' ')) { 777d5e02c3aSGreg Roach $trailingString = ' ' . $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, ' ')) { 798d5e02c3aSGreg Roach $trailingBlanks = ' ' . $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, ' ')) { 816d5e02c3aSGreg Roach $trailingBreaks = ' ' . $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, ' ')) { 843d5e02c3aSGreg Roach $trailingBlanks = ' ' . $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, ' ')) { 904d5e02c3aSGreg Roach $trailingSeparator = ' ' . $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, ' ')) { 927d5e02c3aSGreg Roach $leadingSeparator = ' ' . $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, ' <LTRbr>')) { 955d5e02c3aSGreg Roach $textSpan = str_replace(' <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, ' ')) { 991d5e02c3aSGreg Roach $trailingBlanks = ' ' . $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, ' ')) { 1019d5e02c3aSGreg Roach $trailingString = ' ' . $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, ' ')) { 1045d5e02c3aSGreg Roach $trailingString = ' ' . $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, ' ')) { 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, ' ')) { 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