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 ‎ and ‏ 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 ‎ and ‏ characters should be stripped |
72
|
|
|
* |
73
|
|
|
* @return string The input string, with ‎ and ‏ 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;'], '', $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 = '‌'; // Zero Width Non-Joiner (not sure whether this is still needed to work around a TCPDF bug) |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
155
|
|
|
$endPos = 0; |
156
|
|
|
} |
157
|
|
|
$currentLen += $endPos; |
158
|
|
|
$entity = substr($workingText, 0, $currentLen); |
159
|
|
|
if (strtolower($entity) == ' ') { |
160
|
|
|
$entity .= ' '; // 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 '{': |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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) == ' ') { |
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 === ' ') { |
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) { |
|
|
|
|
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) === ' ') { |
302
|
|
|
// Spaces following this left parenthesis inherit the following directionality too |
303
|
|
|
self::$waitingText .= ' '; |
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) { |
|
|
|
|
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) { |
|
|
|
|
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, ' <LTRbr>') !== false) { |
408
|
|
|
$result = str_replace(' <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, ' <br>') !== false) { |
416
|
|
|
$result = str_replace(' <br>', '<br>', $result); |
417
|
|
|
continue; |
418
|
|
|
} |
419
|
|
|
break; // Neither space nor : 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, ' <RTLbr>') !== false) { |
429
|
|
|
$result = str_replace(' <RTLbr>', '<RTLbr>', $result); |
430
|
|
|
continue; |
431
|
|
|
} |
432
|
|
|
break; // Neither space nor : we're done |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
// Convert '<LTRbr>' and '<RTLbr /' |
|
|
|
|
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>' |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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 is a work-around for a TCPDF bug eating blanks. |
530
|
|
|
$textSpan = str_replace([' <u>', '</u> '], [' <u>', '</u> '], $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); |
|
|
|
|
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) { |
|
|
|
|
617
|
|
|
break; |
618
|
|
|
} // No more possible time strings |
619
|
|
|
$posLRE = strpos($textSpan, WT_UTF8_LRE); |
620
|
|
|
if ($posLRE === false) { |
|
|
|
|
621
|
|
|
break; |
622
|
|
|
} // No more numeric strings |
623
|
|
|
$posPDF = strpos($textSpan, WT_UTF8_PDF, $posLRE); |
624
|
|
|
if ($posPDF === false) { |
|
|
|
|
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) { |
|
|
|
|
633
|
|
|
// Nothing that looks like a time here |
634
|
|
|
$tempResult .= $numericString; |
635
|
|
|
continue; |
636
|
|
|
} |
637
|
|
|
$posBlank = strpos($numericString . ' ', ' '); |
638
|
|
|
$posNbsp = strpos($numericString . ' ', ' '); |
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, ' '); |
657
|
|
|
if ($posBlank === false && $posNbsp === false) { |
|
|
|
|
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) { |
|
|
|
|
665
|
|
|
$posSeparator = $posNbsp; |
666
|
|
|
$lengthSeparator = 6; |
667
|
|
|
} elseif ($posNbsp === false) { |
|
|
|
|
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) === ' ') { |
703
|
|
|
$trailingString = ' ' . $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) { |
|
|
|
|
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) == ' ') { |
729
|
|
|
$trailingString = ' ' . $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) == ' ') { |
750
|
|
|
$trailingBlanks = ' ' . $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) == ' ') { |
768
|
|
|
$trailingBreaks = ' ' . $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) === ' ') { |
794
|
|
|
$trailingBlanks = ' ' . $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) { |
|
|
|
|
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) == ' ') { |
855
|
|
|
$trailingSeparator = ' ' . $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) == ' ') { |
878
|
|
|
$leadingSeparator = ' ' . $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, ' <LTRbr>') !== false) { |
906
|
|
|
$textSpan = str_replace(' <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) == ' ') { |
942
|
|
|
$trailingBlanks = ' ' . $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) === ' ') { |
970
|
|
|
$trailingString = ' ' . $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) { |
|
|
|
|
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) == ' ') { |
996
|
|
|
$trailingString = ' ' . $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) { |
|
|
|
|
1018
|
|
|
break; |
1019
|
|
|
} |
1020
|
|
|
$posStringStart = strrpos(substr($textSpan, 0, $posDashString), '<RTLbr>'); |
1021
|
|
|
if ($posStringStart === false) { |
|
|
|
|
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) == ' ') { |
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) == ' ') { |
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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
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.