FunctionsRtl   F
last analyzed

Complexity

Total Complexity 210

Size/Duplication

Total Lines 1144
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 210
eloc 683
dl 0
loc 1144
rs 1.917
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A stripLrmRlm() 0 3 1
F spanLtrRtl() 0 408 88
A starredName() 0 32 5
F finishCurrentSpan() 0 500 100
A beginCurrentSpan() 0 10 3
A breakCurrentSpan() 0 10 1
A getChar() 0 20 5
B utf8WordWrap() 0 41 7

How to fix   Complexity   

Complex Class

Complex classes like FunctionsRtl often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FunctionsRtl, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2019 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
{
25
    const OPEN_PARENTHESES = '([{';
26
27
    const CLOSE_PARENTHESES = ')]}';
28
29
    const NUMBERS = '0123456789';
30
31
    const NUMBER_PREFIX = '+-'; // Treat these like numbers when at beginning or end of numeric strings
32
33
    const NUMBER_PUNCTUATION = '- ,.:/'; // Treat these like numbers when inside numeric strings
34
35
    const PUNCTUATION = ',.:;?!';
36
37
    /** @var string Were we previously processing LTR or RTL. */
38
    private static $previousState;
39
40
    /** @var string Are we currently processing LTR or RTL. */
41
    private static $currentState;
42
43
    /** @var string Text waiting to be processed. */
44
    private static $waitingText;
45
46
    /** @var string LTR text. */
47
    private static $startLTR;
48
49
    /** @var string LTR text. */
50
    private static $endLTR;
51
52
    /** @var string RTL text. */
53
    private static $startRTL;
54
55
    /** @var string RTL text. */
56
    private static $endRTL;
57
58
    /** @var int Offset into the text. */
59
    private static $lenStart;
60
61
    /** @var int Offset into the text. */
62
    private static $lenEnd;
63
64
    /** @var int Offset into the text. */
65
    private static $posSpanStart;
66
67
    /**
68
     * This function strips &lrm; and &rlm; from the input string. It should be used for all
69
     * text that has been passed through the PrintReady() function before that text is stored
70
     * in the database. The database should NEVER contain these characters.
71
     *
72
     * @param  string $inputText The string from which the &lrm; and &rlm; characters should be stripped
73
     *
74
     * @return string The input string, with &lrm; and &rlm; stripped
75
     */
76
    public static function stripLrmRlm($inputText)
77
    {
78
        return str_replace(array(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);
79
    }
80
81
    /**
82
     * This function encapsulates all texts in the input with <span dir='xxx'> and </span>
83
     * according to the directionality specified.
84
     *
85
     * @param string $inputText Raw input
86
     * @param string $direction Directionality (LTR, BOTH, RTL) default BOTH
87
     * @param string $class Additional text to insert into output <span dir="xxx"> (such as 'class="yyy"')
88
     *
89
     * @return string The string with all texts encapsulated as required
90
     */
91
    public static function spanLtrRtl($inputText, $direction = 'BOTH', $class = '')
92
    {
93
        if ($inputText == '') {
94
            // Nothing to do
95
            return '';
96
        }
97
98
        $workingText = str_replace("\n", '<br>', $inputText);
99
        $workingText = str_replace(array('<span class="starredname"><br>', '<span<br>class="starredname">'), '<br><span class="starredname">', $workingText); // Reposition some incorrectly placed line breaks
100
        $workingText = self::stripLrmRlm($workingText); // Get rid of any existing UTF8 control codes
101
102
        // $nothing  = '&zwnj;'; // Zero Width Non-Joiner  (not sure whether this is still needed to work around a TCPDF bug)
103
        $nothing = '';
104
105
        self::$startLTR = '<LTR>'; // This will become '<span dir="ltr">' at the end
106
        self::$endLTR   = '</LTR>'; // This will become '</span>' at the end
107
        self::$startRTL = '<RTL>'; // This will become '<span dir="rtl">' at the end
108
        self::$endRTL   = '</RTL>'; // This will become '</span>' at the end
109
        self::$lenStart = strlen(self::$startLTR); // RTL version MUST have same length
110
        self::$lenEnd   = strlen(self::$endLTR); // RTL version MUST have same length
111
112
        self::$previousState = '';
113
        self::$currentState  = strtoupper(I18N::direction());
114
        $numberState         = false; // Set when we're inside a numeric string
115
        $result              = '';
116
        self::$waitingText   = '';
117
        $openParDirection    = array();
118
119
        self::beginCurrentSpan($result);
120
121
        while ($workingText != '') {
122
            $charArray     = self::getChar($workingText, 0); // Get the next ASCII or UTF-8 character
123
            $currentLetter = $charArray['letter'];
124
            $currentLen    = $charArray['length'];
125
126
            $openParIndex  = strpos(self::OPEN_PARENTHESES, $currentLetter); // Which opening parenthesis is this?
127
            $closeParIndex = strpos(self::CLOSE_PARENTHESES, $currentLetter); // Which closing parenthesis is this?
128
129
            switch ($currentLetter) {
130
                case '<':
131
                    // Assume this '<' starts an HTML element
132
                    $endPos = strpos($workingText, '>'); // look for the terminating '>'
133
                    if ($endPos === false) {
134
                        $endPos = 0;
135
                    }
136
                    $currentLen += $endPos;
137
                    $element = substr($workingText, 0, $currentLen);
138
                    $temp    = strtolower(substr($element, 0, 3));
139
                    if (strlen($element) < 7 && $temp == '<br') {
140
                        if ($numberState) {
141
                            $numberState = false;
142
                            if (self::$currentState == 'RTL') {
143
                                self::$waitingText .= WT_UTF8_PDF;
144
                            }
145
                        }
146
                        self::breakCurrentSpan($result);
147
                    } elseif (self::$waitingText == '') {
148
                        $result .= $element;
149
                    } else {
150
                        self::$waitingText .= $element;
151
                    }
152
                    $workingText = substr($workingText, $currentLen);
153
                    break;
154
                case '&':
155
                    // Assume this '&' starts an HTML entity
156
                    $endPos = strpos($workingText, ';'); // look for the terminating ';'
157
                    if ($endPos === false) {
158
                        $endPos = 0;
159
                    }
160
                    $currentLen += $endPos;
161
                    $entity = substr($workingText, 0, $currentLen);
162
                    if (strtolower($entity) == '&nbsp;') {
163
                        $entity .= '&nbsp;'; // Ensure consistent case for this entity
164
                    }
165
                    if (self::$waitingText == '') {
166
                        $result .= $entity;
167
                    } else {
168
                        self::$waitingText .= $entity;
169
                    }
170
                    $workingText = substr($workingText, $currentLen);
171
                    break;
172
                case '{':
173
                    if (substr($workingText, 1, 1) == '{') {
174
                        // Assume this '{{' starts a TCPDF directive
175
                        $endPos = strpos($workingText, '}}'); // look for the terminating '}}'
176
                        if ($endPos === false) {
177
                            $endPos = 0;
178
                        }
179
                        $currentLen        = $endPos + 2;
180
                        $directive         = substr($workingText, 0, $currentLen);
181
                        $workingText       = substr($workingText, $currentLen);
182
                        $result            = $result . self::$waitingText . $directive;
183
                        self::$waitingText = '';
184
                        break;
185
                    }
186
                default:
187
                    // Look for strings of numbers with optional leading or trailing + or -
188
                    // and with optional embedded numeric punctuation
189
                    if ($numberState) {
190
                        // If we're inside a numeric string, look for reasons to end it
191
                        $offset    = 0; // Be sure to look at the current character first
192
                        $charArray = self::getChar($workingText . "\n", $offset);
193
                        if (strpos(self::NUMBERS, $charArray['letter']) === false) {
194
                            // This is not a digit. Is it numeric punctuation?
195
                            if (substr($workingText . "\n", $offset, 6) == '&nbsp;') {
196
                                $offset += 6; // This could be numeric punctuation
197
                            } elseif (strpos(self::NUMBER_PUNCTUATION, $charArray['letter']) !== false) {
198
                                $offset += $charArray['length']; // This could be numeric punctuation
199
                            }
200
                            // If the next character is a digit, the current character is numeric punctuation
201
                            $charArray = self::getChar($workingText . "\n", $offset);
202
                            if (strpos(self::NUMBERS, $charArray['letter']) === false) {
203
                                // This is not a digit. End the run of digits and punctuation.
204
                                $numberState = false;
205
                                if (self::$currentState == 'RTL') {
206
                                    if (strpos(self::NUMBER_PREFIX, $currentLetter) === false) {
207
                                        $currentLetter = WT_UTF8_PDF . $currentLetter;
208
                                    } else {
209
                                        $currentLetter = $currentLetter . WT_UTF8_PDF; // Include a trailing + or - in the run
210
                                    }
211
                                }
212
                            }
213
                        }
214
                    } else {
215
                        // If we're outside a numeric string, look for reasons to start it
216
                        if (strpos(self::NUMBER_PREFIX, $currentLetter) !== false) {
217
                            // This might be a number lead-in
218
                            $offset   = $currentLen;
219
                            $nextChar = substr($workingText . "\n", $offset, 1);
220
                            if (strpos(self::NUMBERS, $nextChar) !== false) {
221
                                $numberState = true; // We found a digit: the lead-in is therefore numeric
222
                                if (self::$currentState == 'RTL') {
223
                                    $currentLetter = WT_UTF8_LRE . $currentLetter;
224
                                }
225
                            }
226
                        } elseif (strpos(self::NUMBERS, $currentLetter) !== false) {
227
                            $numberState = true; // The current letter is a digit
228
                            if (self::$currentState == 'RTL') {
229
                                $currentLetter = WT_UTF8_LRE . $currentLetter;
230
                            }
231
                        }
232
                    }
233
234
                    // Determine the directionality of the current UTF-8 character
235
                    $newState = self::$currentState;
236
                    while (true) {
237
                        if (I18N::scriptDirection(I18N::textScript($currentLetter)) === 'rtl') {
238
                            if (self::$currentState == '') {
239
                                $newState = 'RTL';
240
                                break;
241
                            }
242
243
                            if (self::$currentState == 'RTL') {
244
                                break;
245
                            }
246
                            // Switch to RTL only if this isn't a solitary RTL letter
247
                            $tempText = substr($workingText, $currentLen);
248
                            while ($tempText != '') {
249
                                $nextCharArray = self::getChar($tempText, 0);
250
                                $nextLetter    = $nextCharArray['letter'];
251
                                $nextLen       = $nextCharArray['length'];
252
                                $tempText      = substr($tempText, $nextLen);
253
254
                                if (I18N::scriptDirection(I18N::textScript($nextLetter)) === 'rtl') {
255
                                    $newState = 'RTL';
256
                                    break 2;
257
                                }
258
259
                                if (strpos(self::PUNCTUATION, $nextLetter) !== false || strpos(self::OPEN_PARENTHESES, $nextLetter) !== false) {
260
                                    $newState = 'RTL';
261
                                    break 2;
262
                                }
263
264
                                if ($nextLetter === ' ') {
265
                                    break;
266
                                }
267
                                $nextLetter .= substr($tempText . "\n", 0, 5);
268
                                if ($nextLetter === '&nbsp;') {
269
                                    break;
270
                                }
271
                            }
272
                            // This is a solitary RTL letter : wrap it in UTF8 control codes to force LTR directionality
273
                            $currentLetter = WT_UTF8_LRO . $currentLetter . WT_UTF8_PDF;
274
                            $newState      = 'LTR';
275
                            break;
276
                        }
277
                        if (($currentLen != 1) || ($currentLetter >= 'A' && $currentLetter <= 'Z') || ($currentLetter >= 'a' && $currentLetter <= 'z')) {
278
                            // Since it’s neither Hebrew nor Arabic, this UTF-8 character or ASCII letter must be LTR
279
                            $newState = 'LTR';
280
                            break;
281
                        }
282
                        if ($closeParIndex !== false) {
283
                            // This closing parenthesis has to inherit the matching opening parenthesis' directionality
284
                            if (!empty($openParDirection[$closeParIndex]) && $openParDirection[$closeParIndex] != '?') {
285
                                $newState = $openParDirection[$closeParIndex];
286
                            }
287
                            $openParDirection[$closeParIndex] = '';
288
                            break;
289
                        }
290
                        if ($openParIndex !== false) {
291
                            // Opening parentheses always inherit the following directionality
292
                            self::$waitingText .= $currentLetter;
293
                            $workingText = substr($workingText, $currentLen);
294
                            while (true) {
295
                                if ($workingText === '') {
296
                                    break;
297
                                }
298
                                if (substr($workingText, 0, 1) === ' ') {
299
                                    // Spaces following this left parenthesis inherit the following directionality too
300
                                    self::$waitingText .= ' ';
301
                                    $workingText = substr($workingText, 1);
302
                                    continue;
303
                                }
304
                                if (substr($workingText, 0, 6) === '&nbsp;') {
305
                                    // Spaces following this left parenthesis inherit the following directionality too
306
                                    self::$waitingText .= '&nbsp;';
307
                                    $workingText = substr($workingText, 6);
308
                                    continue;
309
                                }
310
                                break;
311
                            }
312
                            $openParDirection[$openParIndex] = '?';
313
                            break 2; // double break because we're waiting for more information
314
                        }
315
316
                        // We have a digit or a "normal" special character.
317
                        //
318
                        // When this character is not at the start of the input string, it inherits the preceding directionality;
319
                        // at the start of the input string, it assumes the following directionality.
320
                        //
321
                        // Exceptions to this rule will be handled later during final clean-up.
322
                        //
323
                        self::$waitingText .= $currentLetter;
324
                        $workingText = substr($workingText, $currentLen);
325
                        if (self::$currentState != '') {
326
                            $result .= self::$waitingText;
327
                            self::$waitingText = '';
328
                        }
329
                        break 2; // double break because we're waiting for more information
330
                    }
331
                    if ($newState != self::$currentState) {
332
                        // A direction change has occurred
333
                        self::finishCurrentSpan($result, false);
334
                        self::$previousState = self::$currentState;
335
                        self::$currentState  = $newState;
336
                        self::beginCurrentSpan($result);
337
                    }
338
                    self::$waitingText .= $currentLetter;
339
                    $workingText = substr($workingText, $currentLen);
340
                    $result .= self::$waitingText;
341
                    self::$waitingText = '';
342
343
                    foreach ($openParDirection as $index => $value) {
344
                        // Since we now know the proper direction, remember it for all waiting opening parentheses
345
                        if ($value === '?') {
346
                            $openParDirection[$index] = self::$currentState;
347
                        }
348
                    }
349
350
                    break;
351
            }
352
        }
353
354
        // We're done. Finish last <span> if necessary
355
        if ($numberState) {
356
            if (self::$waitingText === '') {
357
                if (self::$currentState === 'RTL') {
358
                    $result .= WT_UTF8_PDF;
359
                }
360
            } else {
361
                if (self::$currentState === 'RTL') {
362
                    self::$waitingText .= WT_UTF8_PDF;
363
                }
364
            }
365
        }
366
        self::finishCurrentSpan($result, true);
367
368
        // Get rid of any waiting text
369
        if (self::$waitingText != '') {
370
            if (I18N::direction() === 'rtl' && self::$currentState === 'LTR') {
371
                $result .= self::$startRTL;
372
                $result .= self::$waitingText;
373
                $result .= self::$endRTL;
374
            } else {
375
                $result .= self::$startLTR;
376
                $result .= self::$waitingText;
377
                $result .= self::$endLTR;
378
            }
379
            self::$waitingText = '';
380
        }
381
382
        // Lastly, do some more cleanups
383
384
        // Move leading RTL numeric strings to following LTR text
385
        // (this happens when the page direction is RTL and the original text begins with a number and is followed by LTR text)
386
        while (substr($result, 0, self::$lenStart + 3) === self::$startRTL . WT_UTF8_LRE) {
387
            $spanEnd = strpos($result, self::$endRTL . self::$startLTR);
388
            if ($spanEnd === false) {
389
                break;
390
            }
391
            $textSpan = self::stripLrmRlm(substr($result, self::$lenStart + 3, $spanEnd - self::$lenStart - 3));
392
            if (I18N::scriptDirection(I18N::textScript($textSpan)) === 'rtl') {
393
                break;
394
            }
395
            $result = self::$startLTR . substr($result, self::$lenStart, $spanEnd - self::$lenStart) . substr($result, $spanEnd + self::$lenStart + self::$lenEnd);
396
            break;
397
        }
398
399
        // On RTL pages, put trailing "." in RTL numeric strings into its own RTL span
400
        if (I18N::direction() === 'rtl') {
401
            $result = str_replace(WT_UTF8_PDF . '.' . self::$endRTL, WT_UTF8_PDF . self::$endRTL . self::$startRTL . '.' . self::$endRTL, $result);
402
        }
403
404
        // Trim trailing blanks preceding <br> in LTR text
405
        while (self::$previousState != 'RTL') {
406
            if (strpos($result, ' <LTRbr>') !== false) {
407
                $result = str_replace(' <LTRbr>', '<LTRbr>', $result);
408
                continue;
409
            }
410
            if (strpos($result, '&nbsp;<LTRbr>') !== false) {
411
                $result = str_replace('&nbsp;<LTRbr>', '<LTRbr>', $result);
412
                continue;
413
            }
414
            if (strpos($result, ' <br>') !== false) {
415
                $result = str_replace(' <br>', '<br>', $result);
416
                continue;
417
            }
418
            if (strpos($result, '&nbsp;<br>') !== false) {
419
                $result = str_replace('&nbsp;<br>', '<br>', $result);
420
                continue;
421
            }
422
            break; // Neither space nor &nbsp; : we're done
423
        }
424
425
        // Trim trailing blanks preceding <br> in RTL text
426
        while (true) {
427
            if (strpos($result, ' <RTLbr>') !== false) {
428
                $result = str_replace(' <RTLbr>', '<RTLbr>', $result);
429
                continue;
430
            }
431
            if (strpos($result, '&nbsp;<RTLbr>') !== false) {
432
                $result = str_replace('&nbsp;<RTLbr>', '<RTLbr>', $result);
433
                continue;
434
            }
435
            break; // Neither space nor &nbsp; : we're done
436
        }
437
438
        // Convert '<LTRbr>' and '<RTLbr /'
439
        $result = str_replace(array('<LTRbr>', '<RTLbr>'), array(self::$endLTR . '<br>' . self::$startLTR, self::$endRTL . '<br>' . self::$startRTL), $result);
440
441
        // Include leading indeterminate directional text in whatever follows
442
        if (substr($result . "\n", 0, self::$lenStart) != self::$startLTR && substr($result . "\n", 0, self::$lenStart) != self::$startRTL && substr($result . "\n", 0, 6) != '<br>') {
443
            $leadingText = '';
444
            while (true) {
445
                if ($result == '') {
446
                    $result = $leadingText;
447
                    break;
448
                }
449
                if (substr($result . "\n", 0, self::$lenStart) != self::$startLTR && substr($result . "\n", 0, self::$lenStart) != self::$startRTL) {
450
                    $leadingText .= substr($result, 0, 1);
451
                    $result = substr($result, 1);
452
                    continue;
453
                }
454
                $result = substr($result, 0, self::$lenStart) . $leadingText . substr($result, self::$lenStart);
455
                break;
456
            }
457
        }
458
459
        // Include solitary "-" and "+" in surrounding RTL text
460
        $result = str_replace(array(self::$endRTL . self::$startLTR . '-' . self::$endLTR . self::$startRTL, self::$endRTL . self::$startLTR . '-' . self::$endLTR . self::$startRTL), array('-', '+'), $result);
461
462
        // Remove empty spans
463
        $result = str_replace(array(self::$startLTR . self::$endLTR, self::$startRTL . self::$endRTL), '', $result);
464
465
        // Finally, correct '<LTR>', '</LTR>', '<RTL>', and '</RTL>'
466
        switch ($direction) {
467
            case 'BOTH':
468
            case 'both':
469
                // LTR text: <span dir="ltr"> text </span>
470
                // RTL text: <span dir="rtl"> text </span>
471
                $sLTR = '<span dir="ltr" ' . $class . '>' . $nothing;
472
                $eLTR = $nothing . '</span>';
473
                $sRTL = '<span dir="rtl" ' . $class . '>' . $nothing;
474
                $eRTL = $nothing . '</span>';
475
                break;
476
            case 'LTR':
477
            case 'ltr':
478
                // LTR text: <span dir="ltr"> text </span>
479
                // RTL text: text
480
                $sLTR = '<span dir="ltr" ' . $class . '>' . $nothing;
481
                $eLTR = $nothing . '</span>';
482
                $sRTL = '';
483
                $eRTL = '';
484
                break;
485
            case 'RTL':
486
            case 'rtl':
487
            default:
488
                // LTR text: text
489
                // RTL text: <span dir="rtl"> text </span>
490
                $sLTR = '';
491
                $eLTR = '';
492
                $sRTL = '<span dir="rtl" ' . $class . '>' . $nothing;
493
                $eRTL = $nothing . '</span>';
494
                break;
495
        }
496
        $result = str_replace(array(self::$startLTR, self::$endLTR, self::$startRTL, self::$endRTL), array($sLTR, $eLTR, $sRTL, $eRTL), $result);
497
498
        return $result;
499
    }
500
501
    /**
502
     * Wrap words that have an asterisk suffix in <u> and </u> tags.
503
     * This should underline starred names to show the preferred name.
504
     *
505
     * @param string $textSpan
506
     * @param string $direction
507
     *
508
     * @return string
509
     */
510
    public static function starredName($textSpan, $direction)
511
    {
512
        // To avoid a TCPDF bug that mixes up the word order, insert those <u> and </u> tags
513
        // only when page and span directions are identical.
514
        if ($direction === strtoupper(I18N::direction())) {
515
            while (true) {
516
                $starPos = strpos($textSpan, '*');
517
                if ($starPos === false) {
518
                    break;
519
                }
520
                $trailingText = substr($textSpan, $starPos + 1);
521
                $textSpan     = substr($textSpan, 0, $starPos);
522
                $wordStart    = strrpos($textSpan, ' '); // Find the start of the word
523
                if ($wordStart !== false) {
524
                    $leadingText = substr($textSpan, 0, $wordStart + 1);
525
                    $wordText    = substr($textSpan, $wordStart + 1);
526
                } else {
527
                    $leadingText = '';
528
                    $wordText    = $textSpan;
529
                }
530
                $textSpan = $leadingText . '<u>' . $wordText . '</u>' . $trailingText;
531
            }
532
            $textSpan = preg_replace('~<span class="starredname">(.*)</span>~', '<u>\1</u>', $textSpan);
533
            // The &nbsp; is a work-around for a TCPDF bug eating blanks.
534
            $textSpan = str_replace(array(' <u>', '</u> '), array('&nbsp;<u>', '</u>&nbsp;'), $textSpan);
535
        } else {
536
            // Text and page directions differ:  remove the <span> and </span>
537
            $textSpan = preg_replace('~(.*)\*~', '\1', $textSpan);
538
            $textSpan = preg_replace('~<span class="starredname">(.*)</span>~', '\1', $textSpan);
539
        }
540
541
        return $textSpan;
542
    }
543
544
    /**
545
     * Get the next character from an input string
546
     *
547
     * @param string $text
548
     * @param string $offset
549
     *
550
     * @return array
551
     */
552
    public static function getChar($text, $offset)
553
    {
554
        if ($text == '') {
555
            return array('letter' => '', 'length' => 0);
556
        }
557
558
        $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 $offset 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

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