Completed
Push — develop ( 9087a8...c9b4ef )
by Greg
16:31 queued 05:44
created

FunctionsRtl::stripLrmRlm()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2018 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
namespace Fisharebest\Webtrees\Functions;
17
18
use Fisharebest\Webtrees\I18N;
19
20
/**
21
 * RTL Functions for use in the PDF/HTML reports
22
 */
23
class FunctionsRtl {
24
	const OPEN_PARENTHESES = '([{';
25
26
	const CLOSE_PARENTHESES = ')]}';
27
28
	const NUMBERS = '0123456789';
29
30
	const NUMBER_PREFIX = '+-'; // Treat these like numbers when at beginning or end of numeric strings
31
32
	const NUMBER_PUNCTUATION = '- ,.:/'; // Treat these like numbers when inside numeric strings
33
34
	const PUNCTUATION = ',.:;?!';
35
36
	/** @var string Were we previously processing LTR or RTL. */
37
	private static $previousState;
38
39
	/** @var string Are we currently processing LTR or RTL. */
40
	private static $currentState;
41
42
	/** @var string Text waiting to be processed. */
43
	private static $waitingText;
44
45
	/** @var string LTR text. */
46
	private static $startLTR;
47
48
	/** @var string LTR text. */
49
	private static $endLTR;
50
51
	/** @var string RTL text. */
52
	private static $startRTL;
53
54
	/** @var string RTL text. */
55
	private static $endRTL;
56
57
	/** @var int Offset into the text. */
58
	private static $lenStart;
59
60
	/** @var int Offset into the text. */
61
	private static $lenEnd;
62
63
	/** @var int Offset into the text. */
64
	private static $posSpanStart;
65
66
	/**
67
	 * This function strips &lrm; and &rlm; from the input string. It should be used for all
68
	 * text that has been passed through the PrintReady() function before that text is stored
69
	 * in the database. The database should NEVER contain these characters.
70
	 *
71
	 * @param  string $inputText The string from which the &lrm; and &rlm; characters should be stripped
72
	 *
73
	 * @return string The input string, with &lrm; and &rlm; stripped
74
	 */
75
	public static function stripLrmRlm($inputText) {
76
		return str_replace([WT_UTF8_LRM, WT_UTF8_RLM, WT_UTF8_LRO, WT_UTF8_RLO, WT_UTF8_LRE, WT_UTF8_RLE, WT_UTF8_PDF, '&lrm;', '&rlm;', '&LRM;', '&RLM;'], '', $inputText);
77
	}
78
79
	/**
80
	 * This function encapsulates all texts in the input with <span dir='xxx'> and </span>
81
	 * according to the directionality specified.
82
	 *
83
	 * @param string $inputText Raw input
84
	 * @param string $direction Directionality (LTR, BOTH, RTL) default BOTH
85
	 * @param string $class Additional text to insert into output <span dir="xxx"> (such as 'class="yyy"')
86
	 *
87
	 * @return string The string with all texts encapsulated as required
88
	 */
89
	public static function spanLtrRtl($inputText, $direction = 'BOTH', $class = '') {
90
		if ($inputText == '') {
91
			// Nothing to do
92
			return '';
93
		}
94
95
		$workingText = str_replace("\n", '<br>', $inputText);
96
		$workingText = str_replace(['<span class="starredname"><br>', '<span<br>class="starredname">'], '<br><span class="starredname">', $workingText); // Reposition some incorrectly placed line breaks
97
		$workingText = self::stripLrmRlm($workingText); // Get rid of any existing UTF8 control codes
98
99
		// $nothing  = '&zwnj;'; // Zero Width Non-Joiner  (not sure whether this is still needed to work around a TCPDF bug)
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
100
		$nothing = '';
101
102
		self::$startLTR = '<LTR>'; // This will become '<span dir="ltr">' at the end
103
		self::$endLTR   = '</LTR>'; // This will become '</span>' at the end
104
		self::$startRTL = '<RTL>'; // This will become '<span dir="rtl">' at the end
105
		self::$endRTL   = '</RTL>'; // This will become '</span>' at the end
106
		self::$lenStart = strlen(self::$startLTR); // RTL version MUST have same length
107
		self::$lenEnd   = strlen(self::$endLTR); // RTL version MUST have same length
108
109
		self::$previousState = '';
110
		self::$currentState  = strtoupper(I18N::direction());
111
		$numberState         = false; // Set when we're inside a numeric string
112
		$result              = '';
113
		self::$waitingText   = '';
114
		$openParDirection    = [];
115
116
		self::beginCurrentSpan($result);
117
118
		while ($workingText != '') {
119
			$charArray     = self::getChar($workingText, 0); // Get the next ASCII or UTF-8 character
120
			$currentLetter = $charArray['letter'];
121
			$currentLen    = $charArray['length'];
122
123
			$openParIndex  = strpos(self::OPEN_PARENTHESES, $currentLetter); // Which opening parenthesis is this?
124
			$closeParIndex = strpos(self::CLOSE_PARENTHESES, $currentLetter); // Which closing parenthesis is this?
125
126
			switch ($currentLetter) {
127
				case '<':
128
					// Assume this '<' starts an HTML element
129
					$endPos = strpos($workingText, '>'); // look for the terminating '>'
130
					if ($endPos === false) {
0 ignored issues
show
introduced by
The condition $endPos === false can never be true.
Loading history...
131
						$endPos = 0;
132
					}
133
					$currentLen += $endPos;
134
					$element = substr($workingText, 0, $currentLen);
135
					$temp    = strtolower(substr($element, 0, 3));
136
					if (strlen($element) < 7 && $temp == '<br') {
137
						if ($numberState) {
0 ignored issues
show
introduced by
The condition $numberState can never be true.
Loading history...
138
							$numberState = false;
139
							if (self::$currentState == 'RTL') {
140
								self::$waitingText .= WT_UTF8_PDF;
141
							}
142
						}
143
						self::breakCurrentSpan($result);
144
					} elseif (self::$waitingText == '') {
145
						$result .= $element;
146
					} else {
147
						self::$waitingText .= $element;
148
					}
149
					$workingText = substr($workingText, $currentLen);
150
					break;
151
				case '&':
152
					// Assume this '&' starts an HTML entity
153
					$endPos = strpos($workingText, ';'); // look for the terminating ';'
154
					if ($endPos === false) {
0 ignored issues
show
introduced by
The condition $endPos === false can never be true.
Loading history...
155
						$endPos = 0;
156
					}
157
					$currentLen += $endPos;
158
					$entity = substr($workingText, 0, $currentLen);
159
					if (strtolower($entity) == '&nbsp;') {
160
						$entity .= '&nbsp;'; // Ensure consistent case for this entity
161
					}
162
					if (self::$waitingText == '') {
163
						$result .= $entity;
164
					} else {
165
						self::$waitingText .= $entity;
166
					}
167
					$workingText = substr($workingText, $currentLen);
168
					break;
169
				case '{':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
170
					if (substr($workingText, 1, 1) == '{') {
171
						// Assume this '{{' starts a TCPDF directive
172
						$endPos = strpos($workingText, '}}'); // look for the terminating '}}'
173
						if ($endPos === false) {
0 ignored issues
show
introduced by
The condition $endPos === false can never be true.
Loading history...
174
							$endPos = 0;
175
						}
176
						$currentLen        = $endPos + 2;
177
						$directive         = substr($workingText, 0, $currentLen);
178
						$workingText       = substr($workingText, $currentLen);
179
						$result            = $result . self::$waitingText . $directive;
180
						self::$waitingText = '';
181
						break;
182
					}
183
				default:
184
					// Look for strings of numbers with optional leading or trailing + or -
185
					// and with optional embedded numeric punctuation
186
					if ($numberState) {
0 ignored issues
show
introduced by
The condition $numberState can never be true.
Loading history...
187
						// If we're inside a numeric string, look for reasons to end it
188
						$offset    = 0; // Be sure to look at the current character first
189
						$charArray = self::getChar($workingText . "\n", $offset);
190
						if (strpos(self::NUMBERS, $charArray['letter']) === false) {
191
							// This is not a digit. Is it numeric punctuation?
192
							if (substr($workingText . "\n", $offset, 6) == '&nbsp;') {
193
								$offset += 6; // This could be numeric punctuation
194
							} elseif (strpos(self::NUMBER_PUNCTUATION, $charArray['letter']) !== false) {
195
								$offset += $charArray['length']; // This could be numeric punctuation
196
							}
197
							// If the next character is a digit, the current character is numeric punctuation
198
							$charArray = self::getChar($workingText . "\n", $offset);
199
							if (strpos(self::NUMBERS, $charArray['letter']) === false) {
200
								// This is not a digit. End the run of digits and punctuation.
201
								$numberState = false;
202
								if (self::$currentState == 'RTL') {
203
									if (strpos(self::NUMBER_PREFIX, $currentLetter) === false) {
204
										$currentLetter = WT_UTF8_PDF . $currentLetter;
205
									} else {
206
										$currentLetter = $currentLetter . WT_UTF8_PDF; // Include a trailing + or - in the run
207
									}
208
								}
209
							}
210
						}
211
					} else {
212
						// If we're outside a numeric string, look for reasons to start it
213
						if (strpos(self::NUMBER_PREFIX, $currentLetter) !== false) {
214
							// This might be a number lead-in
215
							$offset   = $currentLen;
216
							$nextChar = substr($workingText . "\n", $offset, 1);
217
							if (strpos(self::NUMBERS, $nextChar) !== false) {
218
								$numberState = true; // We found a digit: the lead-in is therefore numeric
219
								if (self::$currentState == 'RTL') {
220
									$currentLetter = WT_UTF8_LRE . $currentLetter;
221
								}
222
							}
223
						} elseif (strpos(self::NUMBERS, $currentLetter) !== false) {
224
							$numberState = true; // The current letter is a digit
225
							if (self::$currentState == 'RTL') {
226
								$currentLetter = WT_UTF8_LRE . $currentLetter;
227
							}
228
						}
229
					}
230
231
					// Determine the directionality of the current UTF-8 character
232
					$newState = self::$currentState;
233
					while (true) {
234
						if (I18N::scriptDirection(I18N::textScript($currentLetter)) === 'rtl') {
235
							if (self::$currentState == '') {
236
								$newState = 'RTL';
237
								break;
238
							}
239
240
							if (self::$currentState == 'RTL') {
241
								break;
242
							}
243
							// Switch to RTL only if this isn't a solitary RTL letter
244
							$tempText = substr($workingText, $currentLen);
245
							while ($tempText != '') {
246
								$nextCharArray = self::getChar($tempText, 0);
247
								$nextLetter    = $nextCharArray['letter'];
248
								$nextLen       = $nextCharArray['length'];
249
								$tempText      = substr($tempText, $nextLen);
250
251
								if (I18N::scriptDirection(I18N::textScript($nextLetter)) === 'rtl') {
252
									$newState = 'RTL';
253
									break 2;
254
								}
255
256
								if (strpos(self::PUNCTUATION, $nextLetter) !== false || strpos(self::OPEN_PARENTHESES, $nextLetter) !== false) {
257
									$newState = 'RTL';
258
									break 2;
259
								}
260
261
								if ($nextLetter === ' ') {
262
									break;
263
								}
264
								$nextLetter .= substr($tempText . "\n", 0, 5);
265
								if ($nextLetter === '&nbsp;') {
266
									break;
267
								}
268
							}
269
							// This is a solitary RTL letter : wrap it in UTF8 control codes to force LTR directionality
270
							$currentLetter = WT_UTF8_LRO . $currentLetter . WT_UTF8_PDF;
271
							$newState      = 'LTR';
272
							break;
273
						}
274
						if (($currentLen != 1) || ($currentLetter >= 'A' && $currentLetter <= 'Z') || ($currentLetter >= 'a' && $currentLetter <= 'z')) {
275
							// Since it’s neither Hebrew nor Arabic, this UTF-8 character or ASCII letter must be LTR
276
							$newState = 'LTR';
277
							break;
278
						}
279
						if ($closeParIndex !== false) {
0 ignored issues
show
introduced by
The condition $closeParIndex !== false can never be false.
Loading history...
280
							// This closing parenthesis has to inherit the matching opening parenthesis' directionality
281
							if (!empty($openParDirection[$closeParIndex]) && $openParDirection[$closeParIndex] != '?') {
282
								$newState = $openParDirection[$closeParIndex];
283
							}
284
							$openParDirection[$closeParIndex] = '';
285
							break;
286
						}
287
						if ($openParIndex !== false) {
288
							// Opening parentheses always inherit the following directionality
289
							self::$waitingText .= $currentLetter;
290
							$workingText = substr($workingText, $currentLen);
291
							while (true) {
292
								if ($workingText === '') {
293
									break;
294
								}
295
								if (substr($workingText, 0, 1) === ' ') {
296
									// Spaces following this left parenthesis inherit the following directionality too
297
									self::$waitingText .= ' ';
298
									$workingText = substr($workingText, 1);
299
									continue;
300
								}
301
								if (substr($workingText, 0, 6) === '&nbsp;') {
302
									// Spaces following this left parenthesis inherit the following directionality too
303
									self::$waitingText .= '&nbsp;';
304
									$workingText = substr($workingText, 6);
305
									continue;
306
								}
307
								break;
308
							}
309
							$openParDirection[$openParIndex] = '?';
310
							break 2; // double break because we're waiting for more information
311
						}
312
313
						// We have a digit or a "normal" special character.
314
						//
315
						// When this character is not at the start of the input string, it inherits the preceding directionality;
316
						// at the start of the input string, it assumes the following directionality.
317
						//
318
						// Exceptions to this rule will be handled later during final clean-up.
319
						//
320
						self::$waitingText .= $currentLetter;
321
						$workingText = substr($workingText, $currentLen);
322
						if (self::$currentState != '') {
323
							$result .= self::$waitingText;
324
							self::$waitingText = '';
325
						}
326
						break 2; // double break because we're waiting for more information
327
					}
328
					if ($newState != self::$currentState) {
329
						// A direction change has occurred
330
						self::finishCurrentSpan($result, false);
331
						self::$previousState = self::$currentState;
332
						self::$currentState  = $newState;
333
						self::beginCurrentSpan($result);
334
					}
335
					self::$waitingText .= $currentLetter;
336
					$workingText = substr($workingText, $currentLen);
337
					$result .= self::$waitingText;
338
					self::$waitingText = '';
339
340
					foreach ($openParDirection as $index => $value) {
341
						// Since we now know the proper direction, remember it for all waiting opening parentheses
342
						if ($value === '?') {
343
							$openParDirection[$index] = self::$currentState;
344
						}
345
					}
346
347
					break;
348
			}
349
		}
350
351
		// We're done. Finish last <span> if necessary
352
		if ($numberState) {
0 ignored issues
show
introduced by
The condition $numberState can never be true.
Loading history...
353
			if (self::$waitingText === '') {
354
				if (self::$currentState === 'RTL') {
355
					$result .= WT_UTF8_PDF;
356
				}
357
			} else {
358
				if (self::$currentState === 'RTL') {
359
					self::$waitingText .= WT_UTF8_PDF;
360
				}
361
			}
362
		}
363
		self::finishCurrentSpan($result, true);
364
365
		// Get rid of any waiting text
366
		if (self::$waitingText != '') {
367
			if (I18N::direction() === 'rtl' && self::$currentState === 'LTR') {
368
				$result .= self::$startRTL;
369
				$result .= self::$waitingText;
370
				$result .= self::$endRTL;
371
			} else {
372
				$result .= self::$startLTR;
373
				$result .= self::$waitingText;
374
				$result .= self::$endLTR;
375
			}
376
			self::$waitingText = '';
377
		}
378
379
		// Lastly, do some more cleanups
380
381
		// Move leading RTL numeric strings to following LTR text
382
		// (this happens when the page direction is RTL and the original text begins with a number and is followed by LTR text)
383
		while (substr($result, 0, self::$lenStart + 3) === self::$startRTL . WT_UTF8_LRE) {
384
			$spanEnd = strpos($result, self::$endRTL . self::$startLTR);
385
			if ($spanEnd === false) {
0 ignored issues
show
introduced by
The condition $spanEnd === false can never be true.
Loading history...
386
				break;
387
			}
388
			$textSpan = self::stripLrmRlm(substr($result, self::$lenStart + 3, $spanEnd - self::$lenStart - 3));
389
			if (I18N::scriptDirection(I18N::textScript($textSpan)) === 'rtl') {
390
				break;
391
			}
392
			$result = self::$startLTR . substr($result, self::$lenStart, $spanEnd - self::$lenStart) . substr($result, $spanEnd + self::$lenStart + self::$lenEnd);
393
			break;
394
		}
395
396
		// On RTL pages, put trailing "." in RTL numeric strings into its own RTL span
397
		if (I18N::direction() === 'rtl') {
398
			$result = str_replace(WT_UTF8_PDF . '.' . self::$endRTL, WT_UTF8_PDF . self::$endRTL . self::$startRTL . '.' . self::$endRTL, $result);
399
		}
400
401
		// Trim trailing blanks preceding <br> in LTR text
402
		while (self::$previousState != 'RTL') {
403
			if (strpos($result, ' <LTRbr>') !== false) {
404
				$result = str_replace(' <LTRbr>', '<LTRbr>', $result);
405
				continue;
406
			}
407
			if (strpos($result, '&nbsp;<LTRbr>') !== false) {
408
				$result = str_replace('&nbsp;<LTRbr>', '<LTRbr>', $result);
409
				continue;
410
			}
411
			if (strpos($result, ' <br>') !== false) {
412
				$result = str_replace(' <br>', '<br>', $result);
413
				continue;
414
			}
415
			if (strpos($result, '&nbsp;<br>') !== false) {
416
				$result = str_replace('&nbsp;<br>', '<br>', $result);
417
				continue;
418
			}
419
			break; // Neither space nor &nbsp; : we're done
420
		}
421
422
		// Trim trailing blanks preceding <br> in RTL text
423
		while (true) {
424
			if (strpos($result, ' <RTLbr>') !== false) {
425
				$result = str_replace(' <RTLbr>', '<RTLbr>', $result);
426
				continue;
427
			}
428
			if (strpos($result, '&nbsp;<RTLbr>') !== false) {
429
				$result = str_replace('&nbsp;<RTLbr>', '<RTLbr>', $result);
430
				continue;
431
			}
432
			break; // Neither space nor &nbsp; : we're done
433
		}
434
435
		// Convert '<LTRbr>' and '<RTLbr /'
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
436
		$result = str_replace(['<LTRbr>', '<RTLbr>'], [self::$endLTR . '<br>' . self::$startLTR, self::$endRTL . '<br>' . self::$startRTL], $result);
437
438
		// Include leading indeterminate directional text in whatever follows
439
		if (substr($result . "\n", 0, self::$lenStart) != self::$startLTR && substr($result . "\n", 0, self::$lenStart) != self::$startRTL && substr($result . "\n", 0, 6) != '<br>') {
440
			$leadingText = '';
441
			while (true) {
442
				if ($result == '') {
443
					$result = $leadingText;
444
					break;
445
				}
446
				if (substr($result . "\n", 0, self::$lenStart) != self::$startLTR && substr($result . "\n", 0, self::$lenStart) != self::$startRTL) {
447
					$leadingText .= substr($result, 0, 1);
448
					$result = substr($result, 1);
449
					continue;
450
				}
451
				$result = substr($result, 0, self::$lenStart) . $leadingText . substr($result, self::$lenStart);
452
				break;
453
			}
454
		}
455
456
		// Include solitary "-" and "+" in surrounding RTL text
457
		$result = str_replace([self::$endRTL . self::$startLTR . '-' . self::$endLTR . self::$startRTL, self::$endRTL . self::$startLTR . '-' . self::$endLTR . self::$startRTL], ['-', '+'], $result);
458
459
		// Remove empty spans
460
		$result = str_replace([self::$startLTR . self::$endLTR, self::$startRTL . self::$endRTL], '', $result);
461
462
		// Finally, correct '<LTR>', '</LTR>', '<RTL>', and '</RTL>'
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
463
		switch ($direction) {
464
			case 'BOTH':
465
			case 'both':
466
				// LTR text: <span dir="ltr"> text </span>
467
				// RTL text: <span dir="rtl"> text </span>
468
				$sLTR = '<span dir="ltr" ' . $class . '>' . $nothing;
469
				$eLTR = $nothing . '</span>';
470
				$sRTL = '<span dir="rtl" ' . $class . '>' . $nothing;
471
				$eRTL = $nothing . '</span>';
472
				break;
473
			case 'LTR':
474
			case 'ltr':
475
				// LTR text: <span dir="ltr"> text </span>
476
				// RTL text: text
477
				$sLTR = '<span dir="ltr" ' . $class . '>' . $nothing;
478
				$eLTR = $nothing . '</span>';
479
				$sRTL = '';
480
				$eRTL = '';
481
				break;
482
			case 'RTL':
483
			case 'rtl':
484
			default:
485
				// LTR text: text
486
				// RTL text: <span dir="rtl"> text </span>
487
				$sLTR = '';
488
				$eLTR = '';
489
				$sRTL = '<span dir="rtl" ' . $class . '>' . $nothing;
490
				$eRTL = $nothing . '</span>';
491
				break;
492
		}
493
		$result = str_replace([self::$startLTR, self::$endLTR, self::$startRTL, self::$endRTL], [$sLTR, $eLTR, $sRTL, $eRTL], $result);
494
495
		return $result;
496
	}
497
498
	/**
499
	 * Wrap words that have an asterisk suffix in <u> and </u> tags.
500
	 * This should underline starred names to show the preferred name.
501
	 *
502
	 * @param string $textSpan
503
	 * @param string $direction
504
	 *
505
	 * @return string
506
	 */
507
	public static function starredName($textSpan, $direction) {
508
		// To avoid a TCPDF bug that mixes up the word order, insert those <u> and </u> tags
509
		// only when page and span directions are identical.
510
		if ($direction === strtoupper(I18N::direction())) {
511
			while (true) {
512
				$starPos = strpos($textSpan, '*');
513
				if ($starPos === false) {
0 ignored issues
show
introduced by
The condition $starPos === false can never be true.
Loading history...
514
					break;
515
				}
516
				$trailingText = substr($textSpan, $starPos + 1);
517
				$textSpan     = substr($textSpan, 0, $starPos);
518
				$wordStart    = strrpos($textSpan, ' '); // Find the start of the word
519
				if ($wordStart !== false) {
0 ignored issues
show
introduced by
The condition $wordStart !== false can never be false.
Loading history...
520
					$leadingText = substr($textSpan, 0, $wordStart + 1);
521
					$wordText    = substr($textSpan, $wordStart + 1);
522
				} else {
523
					$leadingText = '';
524
					$wordText    = $textSpan;
525
				}
526
				$textSpan = $leadingText . '<u>' . $wordText . '</u>' . $trailingText;
527
			}
528
			$textSpan = preg_replace('~<span class="starredname">(.*)</span>~', '<u>\1</u>', $textSpan);
529
			// The &nbsp; is a work-around for a TCPDF bug eating blanks.
530
			$textSpan = str_replace([' <u>', '</u> '], ['&nbsp;<u>', '</u>&nbsp;'], $textSpan);
531
		} else {
532
			// Text and page directions differ:  remove the <span> and </span>
533
			$textSpan = preg_replace('~(.*)\*~', '\1', $textSpan);
534
			$textSpan = preg_replace('~<span class="starredname">(.*)</span>~', '\1', $textSpan);
535
		}
536
537
		return $textSpan;
538
	}
539
540
	/**
541
	 * Get the next character from an input string
542
	 *
543
	 * @param string $text
544
	 * @param string $offset
545
	 *
546
	 * @return array
547
	 */
548
	public static function getChar($text, $offset) {
549
		if ($text == '') {
550
			return ['letter' => '', 'length' => 0];
551
		}
552
553
		$char   = substr($text, $offset, 1);
0 ignored issues
show
Bug introduced by
$offset of type string is incompatible with the type integer expected by parameter $start of substr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

553
		$char   = substr($text, /** @scrutinizer ignore-type */ $offset, 1);
Loading history...
554
		$length = 1;
555
		if ((ord($char) & 0xE0) == 0xC0) {
556
			$length = 2;
557
		}
558
		if ((ord($char) & 0xF0) == 0xE0) {
559
			$length = 3;
560
		}
561
		if ((ord($char) & 0xF8) == 0xF0) {
562
			$length = 4;
563
		}
564
		$letter = substr($text, $offset, $length);
565
566
		return ['letter' => $letter, 'length' => $length];
567
	}
568
569
	/**
570
	 * Insert <br> into current span
571
	 *
572
	 * @param string $result
573
	 */
574
	public static function breakCurrentSpan(&$result) {
575
		// Interrupt the current span, insert that <br>, and then continue the current span
576
		$result .= self::$waitingText;
577
		self::$waitingText = '';
578
579
		$breakString = '<' . self::$currentState . 'br>';
580
		$result .= $breakString;
581
	}
582
583
	/**
584
	 * Begin current span
585
	 *
586
	 * @param string $result
587
	 */
588
	public static function beginCurrentSpan(&$result) {
589
		if (self::$currentState == 'LTR') {
590
			$result .= self::$startLTR;
591
		}
592
		if (self::$currentState == 'RTL') {
593
			$result .= self::$startRTL;
594
		}
595
596
		self::$posSpanStart = strlen($result);
597
	}
598
599
	/**
600
	 * Finish current span
601
	 *
602
	 * @param string $result
603
	 * @param bool $theEnd
604
	 */
605
	public static function finishCurrentSpan(&$result, $theEnd = false) {
606
		$textSpan = substr($result, self::$posSpanStart);
607
		$result   = substr($result, 0, self::$posSpanStart);
608
609
		// Get rid of empty spans, so that our check for presence of RTL will work
610
		$result = str_replace([self::$startLTR . self::$endLTR, self::$startRTL . self::$endRTL], '', $result);
611
612
		// Look for numeric strings that are times (hh:mm:ss). These have to be separated from surrounding numbers.
613
		$tempResult = '';
614
		while ($textSpan != '') {
615
			$posColon = strpos($textSpan, ':');
616
			if ($posColon === false) {
0 ignored issues
show
introduced by
The condition $posColon === false can never be true.
Loading history...
617
				break;
618
			} // No more possible time strings
619
			$posLRE = strpos($textSpan, WT_UTF8_LRE);
620
			if ($posLRE === false) {
0 ignored issues
show
introduced by
The condition $posLRE === false can never be true.
Loading history...
621
				break;
622
			} // No more numeric strings
623
			$posPDF = strpos($textSpan, WT_UTF8_PDF, $posLRE);
624
			if ($posPDF === false) {
0 ignored issues
show
introduced by
The condition $posPDF === false can never be true.
Loading history...
625
				break;
626
			} // No more numeric strings
627
628
			$tempResult .= substr($textSpan, 0, $posLRE + 3); // Copy everything preceding the numeric string
629
			$numericString = substr($textSpan, $posLRE + 3, $posPDF - $posLRE); // Separate the entire numeric string
630
			$textSpan      = substr($textSpan, $posPDF + 3);
631
			$posColon      = strpos($numericString, ':');
632
			if ($posColon === false) {
0 ignored issues
show
introduced by
The condition $posColon === false can never be true.
Loading history...
633
				// Nothing that looks like a time here
634
				$tempResult .= $numericString;
635
				continue;
636
			}
637
			$posBlank = strpos($numericString . ' ', ' ');
638
			$posNbsp  = strpos($numericString . '&nbsp;', '&nbsp;');
639
			if ($posBlank < $posNbsp) {
640
				$posSeparator    = $posBlank;
641
				$lengthSeparator = 1;
642
			} else {
643
				$posSeparator    = $posNbsp;
644
				$lengthSeparator = 6;
645
			}
646
			if ($posColon > $posSeparator) {
647
				// We have a time string preceded by a blank: Exclude that blank from the numeric string
648
				$tempResult .= substr($numericString, 0, $posSeparator);
649
				$tempResult .= WT_UTF8_PDF;
650
				$tempResult .= substr($numericString, $posSeparator, $lengthSeparator);
651
				$tempResult .= WT_UTF8_LRE;
652
				$numericString = substr($numericString, $posSeparator + $lengthSeparator);
653
			}
654
655
			$posBlank = strpos($numericString, ' ');
656
			$posNbsp  = strpos($numericString, '&nbsp;');
657
			if ($posBlank === false && $posNbsp === false) {
0 ignored issues
show
introduced by
The condition $posBlank === false && $posNbsp === false can never be true.
Loading history...
658
				// The time string isn't followed by a blank
659
				$textSpan = $numericString . $textSpan;
660
				continue;
661
			}
662
663
			// We have a time string followed by a blank: Exclude that blank from the numeric string
664
			if ($posBlank === false) {
0 ignored issues
show
introduced by
The condition $posBlank === false can never be true.
Loading history...
665
				$posSeparator    = $posNbsp;
666
				$lengthSeparator = 6;
667
			} elseif ($posNbsp === false) {
0 ignored issues
show
introduced by
The condition $posNbsp === false can never be true.
Loading history...
668
				$posSeparator    = $posBlank;
669
				$lengthSeparator = 1;
670
			} elseif ($posBlank < $posNbsp) {
671
				$posSeparator    = $posBlank;
672
				$lengthSeparator = 1;
673
			} else {
674
				$posSeparator    = $posNbsp;
675
				$lengthSeparator = 6;
676
			}
677
			$tempResult .= substr($numericString, 0, $posSeparator);
678
			$tempResult .= WT_UTF8_PDF;
679
			$tempResult .= substr($numericString, $posSeparator, $lengthSeparator);
680
			$posSeparator += $lengthSeparator;
681
			$numericString = substr($numericString, $posSeparator);
682
			$textSpan      = WT_UTF8_LRE . $numericString . $textSpan;
683
		}
684
		$textSpan       = $tempResult . $textSpan;
685
		$trailingBlanks = '';
686
		$trailingBreaks = '';
687
688
		/* ****************************** LTR text handling ******************************** */
689
690
		if (self::$currentState === 'LTR') {
691
			// Move trailing numeric strings to the following RTL text. Include any blanks preceding or following the numeric text too.
692
			if (I18N::direction() === 'rtl' && self::$previousState === 'RTL' && !$theEnd) {
693
				$trailingString = '';
694
				$savedSpan      = $textSpan;
695
				while ($textSpan !== '') {
696
					// Look for trailing spaces and tentatively move them
697
					if (substr($textSpan, -1) === ' ') {
698
						$trailingString = ' ' . $trailingString;
699
						$textSpan       = substr($textSpan, 0, -1);
700
						continue;
701
					}
702
					if (substr($textSpan, -6) === '&nbsp;') {
703
						$trailingString = '&nbsp;' . $trailingString;
704
						$textSpan       = substr($textSpan, 0, -1);
705
						continue;
706
					}
707
					if (substr($textSpan, -3) !== WT_UTF8_PDF) {
708
						// There is no trailing numeric string
709
						$textSpan = $savedSpan;
710
						break;
711
					}
712
713
					// We have a numeric string
714
					$posStartNumber = strrpos($textSpan, WT_UTF8_LRE);
715
					if ($posStartNumber === false) {
0 ignored issues
show
introduced by
The condition $posStartNumber === false can never be true.
Loading history...
716
						$posStartNumber = 0;
717
					}
718
					$trailingString = substr($textSpan, $posStartNumber, strlen($textSpan) - $posStartNumber) . $trailingString;
719
					$textSpan       = substr($textSpan, 0, $posStartNumber);
720
721
					// Look for more spaces and move them too
722
					while ($textSpan != '') {
723
						if (substr($textSpan, -1) == ' ') {
724
							$trailingString = ' ' . $trailingString;
725
							$textSpan       = substr($textSpan, 0, -1);
726
							continue;
727
						}
728
						if (substr($textSpan, -6) == '&nbsp;') {
729
							$trailingString = '&nbsp;' . $trailingString;
730
							$textSpan       = substr($textSpan, 0, -1);
731
							continue;
732
						}
733
						break;
734
					}
735
736
					self::$waitingText = $trailingString . self::$waitingText;
737
					break;
738
				}
739
			}
740
741
			$savedSpan = $textSpan;
742
			// Move any trailing <br>, optionally preceded or followed by blanks, outside this LTR span
743
			while ($textSpan != '') {
744
				if (substr($textSpan, -1) == ' ') {
745
					$trailingBlanks = ' ' . $trailingBlanks;
746
					$textSpan       = substr($textSpan, 0, -1);
747
					continue;
748
				}
749
				if (substr('......' . $textSpan, -6) == '&nbsp;') {
750
					$trailingBlanks = '&nbsp;' . $trailingBlanks;
751
					$textSpan       = substr($textSpan, 0, -6);
752
					continue;
753
				}
754
				break;
755
			}
756
			while (substr($textSpan, -9) == '<LTRbr>') {
757
				$trailingBreaks = '<br>' . $trailingBreaks; // Plain <br> because it’s outside a span
758
				$textSpan       = substr($textSpan, 0, -9);
759
			}
760
			if ($trailingBreaks != '') {
761
				while ($textSpan != '') {
762
					if (substr($textSpan, -1) == ' ') {
763
						$trailingBreaks = ' ' . $trailingBreaks;
764
						$textSpan       = substr($textSpan, 0, -1);
765
						continue;
766
					}
767
					if (substr('......' . $textSpan, -6) == '&nbsp;') {
768
						$trailingBreaks = '&nbsp;' . $trailingBreaks;
769
						$textSpan       = substr($textSpan, 0, -6);
770
						continue;
771
					}
772
					break;
773
				}
774
				self::$waitingText = $trailingBlanks . self::$waitingText; // Put those trailing blanks inside the following span
775
			} else {
776
				$textSpan = $savedSpan;
777
			}
778
779
			$trailingBlanks      = '';
780
			$trailingPunctuation = '';
781
			$trailingID          = '';
782
			$trailingSeparator   = '';
783
			$leadingSeparator    = '';
784
			while (I18N::direction() === 'rtl') {
785
				if (strpos($result, self::$startRTL) !== false) {
786
					// Remove trailing blanks for inclusion in a separate LTR span
787
					while ($textSpan != '') {
788
						if (substr($textSpan, -1) === ' ') {
789
							$trailingBlanks = ' ' . $trailingBlanks;
790
							$textSpan       = substr($textSpan, 0, -1);
791
							continue;
792
						}
793
						if (substr($textSpan, -6) === '&nbsp;') {
794
							$trailingBlanks = '&nbsp;' . $trailingBlanks;
795
							$textSpan       = substr($textSpan, 0, -1);
796
							continue;
797
						}
798
						break;
799
					}
800
801
					// Remove trailing punctuation for inclusion in a separate LTR span
802
					if ($textSpan == '') {
803
						$trailingChar = "\n";
804
					} else {
805
						$trailingChar = substr($textSpan, -1);
806
					}
807
					if (strpos(self::PUNCTUATION, $trailingChar) !== false) {
808
						$trailingPunctuation = $trailingChar;
809
						$textSpan            = substr($textSpan, 0, -1);
810
					}
811
				}
812
813
				// Remove trailing ID numbers that look like "(xnnn)" for inclusion in a separate LTR span
814
				while (true) {
815
					if (substr($textSpan, -1) != ')') {
816
						break;
817
					} // There is no trailing ')'
818
					$posLeftParen = strrpos($textSpan, '(');
819
					if ($posLeftParen === false) {
0 ignored issues
show
introduced by
The condition $posLeftParen === false can never be true.
Loading history...
820
						break;
821
					} // There is no leading '('
822
					$temp = self::stripLrmRlm(substr($textSpan, $posLeftParen)); // Get rid of UTF8 control codes
823
824
					// If the parenthesized text doesn't look like an ID number,
825
					// we don't want to touch it.
826
					// This check won’t work if somebody uses ID numbers with an unusual format.
827
					$offset    = 1;
828
					$charArray = self::getChar($temp, $offset); // Get 1st character of parenthesized text
829
					if (strpos(self::NUMBERS, $charArray['letter']) !== false) {
830
						break;
831
					}
832
					$offset += $charArray['length']; // Point at 2nd character of parenthesized text
833
					if (strpos(self::NUMBERS, substr($temp, $offset, 1)) === false) {
834
						break;
835
					}
836
					// 1st character of parenthesized text is alpha, 2nd character is a digit; last has to be a digit too
837
					if (strpos(self::NUMBERS, substr($temp, -2, 1)) === false) {
838
						break;
839
					}
840
841
					$trailingID = substr($textSpan, $posLeftParen);
842
					$textSpan   = substr($textSpan, 0, $posLeftParen);
843
					break;
844
				}
845
846
				// Look for " - " or blank preceding the ID number and remove it for inclusion in a separate LTR span
847
				if ($trailingID != '') {
848
					while ($textSpan != '') {
849
						if (substr($textSpan, -1) == ' ') {
850
							$trailingSeparator = ' ' . $trailingSeparator;
851
							$textSpan          = substr($textSpan, 0, -1);
852
							continue;
853
						}
854
						if (substr($textSpan, -6) == '&nbsp;') {
855
							$trailingSeparator = '&nbsp;' . $trailingSeparator;
856
							$textSpan          = substr($textSpan, 0, -6);
857
							continue;
858
						}
859
						if (substr($textSpan, -1) == '-') {
860
							$trailingSeparator = '-' . $trailingSeparator;
861
							$textSpan          = substr($textSpan, 0, -1);
862
							continue;
863
						}
864
						break;
865
					}
866
				}
867
868
				// Look for " - " preceding the text and remove it for inclusion in a separate LTR span
869
				$foundSeparator = false;
870
				$savedSpan      = $textSpan;
871
				while ($textSpan != '') {
872
					if (substr($textSpan, 0, 1) == ' ') {
873
						$leadingSeparator = ' ' . $leadingSeparator;
874
						$textSpan         = substr($textSpan, 1);
875
						continue;
876
					}
877
					if (substr($textSpan, 0, 6) == '&nbsp;') {
878
						$leadingSeparator = '&nbsp;' . $leadingSeparator;
879
						$textSpan         = substr($textSpan, 6);
880
						continue;
881
					}
882
					if (substr($textSpan, 0, 1) == '-') {
883
						$leadingSeparator = '-' . $leadingSeparator;
884
						$textSpan         = substr($textSpan, 1);
885
						$foundSeparator   = true;
886
						continue;
887
					}
888
					break;
889
				}
890
				if (!$foundSeparator) {
891
					$textSpan         = $savedSpan;
892
					$leadingSeparator = '';
893
				}
894
				break;
895
			}
896
897
			// We're done: finish the span
898
			$textSpan = self::starredName($textSpan, 'LTR'); // Wrap starred name in <u> and </u> tags
899
			while (true) {
900
				// Remove blanks that precede <LTRbr>
901
				if (strpos($textSpan, ' <LTRbr>') !== false) {
902
					$textSpan = str_replace(' <LTRbr>', '<LTRbr>', $textSpan);
903
					continue;
904
				}
905
				if (strpos($textSpan, '&nbsp;<LTRbr>') !== false) {
906
					$textSpan = str_replace('&nbsp;<LTRbr>', '<LTRbr>', $textSpan);
907
					continue;
908
				}
909
				break;
910
			}
911
			if ($leadingSeparator != '') {
912
				$result = $result . self::$startLTR . $leadingSeparator . self::$endLTR;
913
			}
914
			$result = $result . $textSpan . self::$endLTR;
915
			if ($trailingSeparator != '') {
916
				$result = $result . self::$startLTR . $trailingSeparator . self::$endLTR;
917
			}
918
			if ($trailingID != '') {
919
				$result = $result . self::$startLTR . $trailingID . self::$endLTR;
920
			}
921
			if ($trailingPunctuation != '') {
922
				$result = $result . self::$startLTR . $trailingPunctuation . self::$endLTR;
923
			}
924
			if ($trailingBlanks != '') {
925
				$result = $result . self::$startLTR . $trailingBlanks . self::$endLTR;
926
			}
927
		}
928
929
		/* ****************************** RTL text handling ******************************** */
930
931
		if (self::$currentState == 'RTL') {
932
			$savedSpan = $textSpan;
933
934
			// Move any trailing <br>, optionally followed by blanks, outside this RTL span
935
			while ($textSpan != '') {
936
				if (substr($textSpan, -1) == ' ') {
937
					$trailingBlanks = ' ' . $trailingBlanks;
938
					$textSpan       = substr($textSpan, 0, -1);
939
					continue;
940
				}
941
				if (substr('......' . $textSpan, -6) == '&nbsp;') {
942
					$trailingBlanks = '&nbsp;' . $trailingBlanks;
943
					$textSpan       = substr($textSpan, 0, -6);
944
					continue;
945
				}
946
				break;
947
			}
948
			while (substr($textSpan, -9) == '<RTLbr>') {
949
				$trailingBreaks = '<br>' . $trailingBreaks; // Plain <br> because it’s outside a span
950
				$textSpan       = substr($textSpan, 0, -9);
951
			}
952
			if ($trailingBreaks != '') {
953
				self::$waitingText = $trailingBlanks . self::$waitingText; // Put those trailing blanks inside the following span
954
			} else {
955
				$textSpan = $savedSpan;
956
			}
957
958
			// Move trailing numeric strings to the following LTR text. Include any blanks preceding or following the numeric text too.
959
			if (!$theEnd && I18N::direction() !== 'rtl') {
960
				$trailingString = '';
961
				$savedSpan      = $textSpan;
962
				while ($textSpan != '') {
963
					// Look for trailing spaces and tentatively move them
964
					if (substr($textSpan, -1) === ' ') {
965
						$trailingString = ' ' . $trailingString;
966
						$textSpan       = substr($textSpan, 0, -1);
967
						continue;
968
					}
969
					if (substr($textSpan, -6) === '&nbsp;') {
970
						$trailingString = '&nbsp;' . $trailingString;
971
						$textSpan       = substr($textSpan, 0, -1);
972
						continue;
973
					}
974
					if (substr($textSpan, -3) !== WT_UTF8_PDF) {
975
						// There is no trailing numeric string
976
						$textSpan = $savedSpan;
977
						break;
978
					}
979
980
					// We have a numeric string
981
					$posStartNumber = strrpos($textSpan, WT_UTF8_LRE);
982
					if ($posStartNumber === false) {
0 ignored issues
show
introduced by
The condition $posStartNumber === false can never be true.
Loading history...
983
						$posStartNumber = 0;
984
					}
985
					$trailingString = substr($textSpan, $posStartNumber, strlen($textSpan) - $posStartNumber) . $trailingString;
986
					$textSpan       = substr($textSpan, 0, $posStartNumber);
987
988
					// Look for more spaces and move them too
989
					while ($textSpan != '') {
990
						if (substr($textSpan, -1) == ' ') {
991
							$trailingString = ' ' . $trailingString;
992
							$textSpan       = substr($textSpan, 0, -1);
993
							continue;
994
						}
995
						if (substr($textSpan, -6) == '&nbsp;') {
996
							$trailingString = '&nbsp;' . $trailingString;
997
							$textSpan       = substr($textSpan, 0, -1);
998
							continue;
999
						}
1000
						break;
1001
					}
1002
1003
					self::$waitingText = $trailingString . self::$waitingText;
1004
					break;
1005
				}
1006
			}
1007
1008
			// Trailing " - " needs to be prefixed to the following span
1009
			if (!$theEnd && substr('...' . $textSpan, -3) == ' - ') {
1010
				$textSpan          = substr($textSpan, 0, -3);
1011
				self::$waitingText = ' - ' . self::$waitingText;
1012
			}
1013
1014
			while (I18N::direction() === 'rtl') {
1015
				// Look for " - " preceding <RTLbr> and relocate it to the front of the string
1016
				$posDashString = strpos($textSpan, ' - <RTLbr>');
1017
				if ($posDashString === false) {
0 ignored issues
show
introduced by
The condition $posDashString === false can never be true.
Loading history...
1018
					break;
1019
				}
1020
				$posStringStart = strrpos(substr($textSpan, 0, $posDashString), '<RTLbr>');
1021
				if ($posStringStart === false) {
0 ignored issues
show
introduced by
The condition $posStringStart === false can never be true.
Loading history...
1022
					$posStringStart = 0;
1023
				} else {
1024
					$posStringStart += 9;
1025
				} // Point to the first char following the last <RTLbr>
1026
1027
				$textSpan = substr($textSpan, 0, $posStringStart) . ' - ' . substr($textSpan, $posStringStart, $posDashString - $posStringStart) . substr($textSpan, $posDashString + 3);
1028
			}
1029
1030
			// Strip leading spaces from the RTL text
1031
			$countLeadingSpaces = 0;
1032
			while ($textSpan != '') {
1033
				if (substr($textSpan, 0, 1) == ' ') {
1034
					$countLeadingSpaces++;
1035
					$textSpan = substr($textSpan, 1);
1036
					continue;
1037
				}
1038
				if (substr($textSpan, 0, 6) == '&nbsp;') {
1039
					$countLeadingSpaces++;
1040
					$textSpan = substr($textSpan, 6);
1041
					continue;
1042
				}
1043
				break;
1044
			}
1045
1046
			// Strip trailing spaces from the RTL text
1047
			$countTrailingSpaces = 0;
1048
			while ($textSpan != '') {
1049
				if (substr($textSpan, -1) == ' ') {
1050
					$countTrailingSpaces++;
1051
					$textSpan = substr($textSpan, 0, -1);
1052
					continue;
1053
				}
1054
				if (substr($textSpan, -6) == '&nbsp;') {
1055
					$countTrailingSpaces++;
1056
					$textSpan = substr($textSpan, 0, -6);
1057
					continue;
1058
				}
1059
				break;
1060
			}
1061
1062
			// Look for trailing " -", reverse it, and relocate it to the front of the string
1063
			if (substr($textSpan, -2) === ' -') {
1064
				$posDashString  = strlen($textSpan) - 2;
1065
				$posStringStart = strrpos(substr($textSpan, 0, $posDashString), '<RTLbr>');
1066
				if ($posStringStart === false) {
0 ignored issues
show
introduced by
The condition $posStringStart === false can never be true.
Loading history...
1067
					$posStringStart = 0;
1068
				} else {
1069
					$posStringStart += 9;
1070
				} // Point to the first char following the last <RTLbr>
1071
1072
				$textSpan = substr($textSpan, 0, $posStringStart) . '- ' . substr($textSpan, $posStringStart, $posDashString - $posStringStart) . substr($textSpan, $posDashString + 2);
1073
			}
1074
1075
			if ($countLeadingSpaces != 0) {
0 ignored issues
show
introduced by
The condition $countLeadingSpaces != 0 can never be true.
Loading history...
1076
				$newLength = strlen($textSpan) + $countLeadingSpaces;
1077
				$textSpan  = str_pad($textSpan, $newLength, ' ', (I18N::direction() === 'rtl' ? STR_PAD_LEFT : STR_PAD_RIGHT));
1078
			}
1079
			if ($countTrailingSpaces != 0) {
0 ignored issues
show
introduced by
The condition $countTrailingSpaces != 0 can never be true.
Loading history...
1080
				if (I18N::direction() === 'ltr') {
1081
					if ($trailingBreaks === '') {
1082
						// Move trailing RTL spaces to front of following LTR span
1083
						$newLength         = strlen(self::$waitingText) + $countTrailingSpaces;
1084
						self::$waitingText = str_pad(self::$waitingText, $newLength, ' ', STR_PAD_LEFT);
1085
					}
1086
				} else {
1087
					$newLength = strlen($textSpan) + $countTrailingSpaces;
1088
					$textSpan  = str_pad($textSpan, $newLength, ' ', STR_PAD_RIGHT);
1089
				}
1090
			}
1091
1092
			// We're done: finish the span
1093
			$textSpan = self::starredName($textSpan, 'RTL'); // Wrap starred name in <u> and </u> tags
1094
			$result   = $result . $textSpan . self::$endRTL;
1095
		}
1096
1097
		if (self::$currentState != 'LTR' && self::$currentState != 'RTL') {
1098
			$result = $result . $textSpan;
1099
		}
1100
1101
		$result .= $trailingBreaks; // Get rid of any waiting <br>
1102
	}
1103
1104
	/**
1105
	 * Wrap text, similar to the PHP wordwrap() function.
1106
	 *
1107
	 * @param string $string
1108
	 * @param int $width
1109
	 * @param string $sep
1110
	 * @param bool $cut
1111
	 *
1112
	 * @return string
1113
	 */
1114
	public static function utf8WordWrap($string, $width = 75, $sep = "\n", $cut = false) {
1115
		$out = '';
1116
		while ($string) {
1117
			if (mb_strlen($string) <= $width) {
1118
				// Do not wrap any text that is less than the output area.
1119
				$out .= $string;
1120
				$string = '';
1121
			} else {
1122
				$sub1 = mb_substr($string, 0, $width + 1);
1123
				if (mb_substr($string, mb_strlen($sub1) - 1, 1) == ' ') {
1124
					// include words that end by a space immediately after the area.
1125
					$sub = $sub1;
1126
				} else {
1127
					$sub = mb_substr($string, 0, $width);
1128
				}
1129
				$spacepos = strrpos($sub, ' ');
1130
				if ($spacepos === false) {
0 ignored issues
show
introduced by
The condition $spacepos === false can never be true.
Loading history...
1131
					// No space on line?
1132
					if ($cut) {
1133
						$out .= $sub . $sep;
1134
						$string = mb_substr($string, mb_strlen($sub));
1135
					} else {
1136
						$spacepos = strpos($string, ' ');
1137
						if ($spacepos === false) {
1138
							$out .= $string;
1139
							$string = '';
1140
						} else {
1141
							$out .= substr($string, 0, $spacepos) . $sep;
1142
							$string = substr($string, $spacepos + 1);
1143
						}
1144
					}
1145
				} else {
1146
					// Split at space;
1147
					$out .= substr($string, 0, $spacepos) . $sep;
1148
					$string = substr($string, $spacepos + 1);
1149
				}
1150
			}
1151
		}
1152
1153
		return $out;
1154
	}
1155
}
1156