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