• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 12673986434

08 Jan 2025 03:42PM UTC coverage: 84.455% (+0.001%) from 84.454%
12673986434

Pull #9385

github

web-flow
Merge 06e47f0ee into e475fd8fa
Pull Request #9385: refactor: Fix phpstan expr.resultUnused

20699 of 24509 relevant lines covered (84.45%)

190.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.76
/system/Helpers/text_helper.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
use CodeIgniter\Exceptions\InvalidArgumentException;
15
use Config\ForeignCharacters;
16

17
// CodeIgniter Text Helpers
18

19
if (! function_exists('word_limiter')) {
1✔
20
    /**
21
     * Word Limiter
22
     *
23
     * Limits a string to X number of words.
24
     *
25
     * @param string $endChar the end character. Usually an ellipsis
26
     */
27
    function word_limiter(string $str, int $limit = 100, string $endChar = '&#8230;'): string
28
    {
29
        if (trim($str) === '') {
2✔
30
            return $str;
1✔
31
        }
32

33
        preg_match('/^\s*+(?:\S++\s*+){1,' . $limit . '}/', $str, $matches);
2✔
34

35
        if (strlen($str) === strlen($matches[0])) {
2✔
36
            $endChar = '';
1✔
37
        }
38

39
        return rtrim($matches[0]) . $endChar;
2✔
40
    }
41
}
42

43
if (! function_exists('character_limiter')) {
1✔
44
    /**
45
     * Character Limiter
46
     *
47
     * Limits the string based on the character count. Preserves complete words
48
     * so the character count may not be exactly as specified.
49
     *
50
     * @param string $endChar the end character. Usually an ellipsis
51
     */
52
    function character_limiter(string $string, int $limit = 500, string $endChar = '&#8230;'): string
53
    {
54
        if (mb_strlen($string) < $limit) {
2✔
55
            return $string;
1✔
56
        }
57

58
        // a bit complicated, but faster than preg_replace with \s+
59
        $string       = preg_replace('/ {2,}/', ' ', str_replace(["\r", "\n", "\t", "\x0B", "\x0C"], ' ', $string));
2✔
60
        $stringLength = mb_strlen($string);
2✔
61

62
        if ($stringLength <= $limit) {
2✔
63
            return $string;
1✔
64
        }
65

66
        $output       = '';
2✔
67
        $outputLength = 0;
2✔
68
        $words        = explode(' ', trim($string));
2✔
69

70
        foreach ($words as $word) {
2✔
71
            $output .= $word . ' ';
2✔
72
            $outputLength = mb_strlen($output);
2✔
73

74
            if ($outputLength >= $limit) {
2✔
75
                $output = trim($output);
2✔
76
                break;
2✔
77
            }
78
        }
79

80
        return ($outputLength === $stringLength) ? $output : $output . $endChar;
2✔
81
    }
82
}
83

84
if (! function_exists('ascii_to_entities')) {
1✔
85
    /**
86
     * High ASCII to Entities
87
     *
88
     * Converts high ASCII text and MS Word special characters to character entities
89
     */
90
    function ascii_to_entities(string $str): string
91
    {
92
        $out = '';
1✔
93

94
        for ($i = 0, $s = strlen($str) - 1, $count = 1, $temp = []; $i <= $s; $i++) {
1✔
95
            $ordinal = ord($str[$i]);
1✔
96

97
            if ($ordinal < 128) {
1✔
98
                /*
99
                  If the $temp array has a value but we have moved on, then it seems only
100
                  fair that we output that entity and restart $temp before continuing.
101
                 */
102
                if (count($temp) === 1) {
1✔
103
                    $out .= '&#' . array_shift($temp) . ';';
×
104
                    $count = 1;
×
105
                }
106

107
                $out .= $str[$i];
1✔
108
            } else {
109
                if ($temp === []) {
1✔
110
                    $count = ($ordinal < 224) ? 2 : 3;
1✔
111
                }
112

113
                $temp[] = $ordinal;
1✔
114

115
                if (count($temp) === $count) {
1✔
116
                    $number = ($count === 3) ? (($temp[0] % 16) * 4096) + (($temp[1] % 64) * 64) + ($temp[2] % 64) : (($temp[0] % 32) * 64) + ($temp[1] % 64);
1✔
117
                    $out .= '&#' . $number . ';';
1✔
118
                    $count = 1;
1✔
119
                    $temp  = [];
1✔
120
                }
121
                // If this is the last iteration, just output whatever we have
122
                elseif ($i === $s) {
1✔
123
                    $out .= '&#' . implode(';', $temp) . ';';
×
124
                }
125
            }
126
        }
127

128
        return $out;
1✔
129
    }
130
}
131

132
if (! function_exists('entities_to_ascii')) {
1✔
133
    /**
134
     * Entities to ASCII
135
     *
136
     * Converts character entities back to ASCII
137
     */
138
    function entities_to_ascii(string $str, bool $all = true): string
139
    {
140
        if (preg_match_all('/\&#(\d+)\;/', $str, $matches) >= 1) {
3✔
141
            for ($i = 0, $s = count($matches[0]); $i < $s; $i++) {
2✔
142
                $digits = (int) $matches[1][$i];
2✔
143
                $out    = '';
2✔
144
                if ($digits < 128) {
2✔
145
                    $out .= chr($digits);
1✔
146
                } elseif ($digits < 2048) {
1✔
147
                    $out .= chr(192 + (($digits - ($digits % 64)) / 64)) . chr(128 + ($digits % 64));
1✔
148
                } else {
149
                    $out .= chr(224 + (($digits - ($digits % 4096)) / 4096))
1✔
150
                            . chr(128 + ((($digits % 4096) - ($digits % 64)) / 64))
1✔
151
                            . chr(128 + ($digits % 64));
1✔
152
                }
153
                $str = str_replace($matches[0][$i], $out, $str);
2✔
154
            }
155
        }
156

157
        if ($all) {
3✔
158
            return str_replace(
3✔
159
                ['&amp;', '&lt;', '&gt;', '&quot;', '&apos;', '&#45;'],
3✔
160
                ['&', '<', '>', '"', "'", '-'],
3✔
161
                $str
3✔
162
            );
3✔
163
        }
164

165
        return $str;
1✔
166
    }
167
}
168

169
if (! function_exists('word_censor')) {
1✔
170
    /**
171
     * Word Censoring Function
172
     *
173
     * Supply a string and an array of disallowed words and any
174
     * matched words will be converted to #### or to the replacement
175
     * word you've submitted.
176
     *
177
     * @param string $str         the text string
178
     * @param array  $censored    the array of censored words
179
     * @param string $replacement the optional replacement value
180
     */
181
    function word_censor(string $str, array $censored, string $replacement = ''): string
182
    {
183
        if ($censored === []) {
3✔
184
            return $str;
1✔
185
        }
186

187
        $str = ' ' . $str . ' ';
2✔
188

189
        // \w, \b and a few others do not match on a unicode character
190
        // set for performance reasons. As a result words like über
191
        // will not match on a word boundary. Instead, we'll assume that
192
        // a bad word will be bookended by any of these characters.
193
        $delim = '[-_\'\"`(){}<>\[\]|!?@#%&,.:;^~*+=\/ 0-9\n\r\t]';
2✔
194

195
        foreach ($censored as $badword) {
2✔
196
            $badword = str_replace('\*', '\w*?', preg_quote($badword, '/'));
2✔
197

198
            if ($replacement !== '') {
2✔
199
                $str = preg_replace(
1✔
200
                    "/({$delim})(" . $badword . ")({$delim})/i",
1✔
201
                    "\\1{$replacement}\\3",
1✔
202
                    $str
1✔
203
                );
1✔
204
            } elseif (preg_match_all("/{$delim}(" . $badword . "){$delim}/i", $str, $matches, PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE) >= 1) {
1✔
205
                $matches = $matches[1];
1✔
206

207
                for ($i = count($matches) - 1; $i >= 0; $i--) {
1✔
208
                    $length = strlen($matches[$i][0]);
1✔
209

210
                    $str = substr_replace(
1✔
211
                        $str,
1✔
212
                        str_repeat('#', $length),
1✔
213
                        $matches[$i][1],
1✔
214
                        $length
1✔
215
                    );
1✔
216
                }
217
            }
218
        }
219

220
        return trim($str);
2✔
221
    }
222
}
223

224
if (! function_exists('highlight_code')) {
1✔
225
    /**
226
     * Code Highlighter
227
     *
228
     * Colorizes code strings
229
     *
230
     * @param string $str the text string
231
     */
232
    function highlight_code(string $str): string
233
    {
234
        /* The highlight string function encodes and highlights
235
         * brackets so we need them to start raw.
236
         *
237
         * Also replace any existing PHP tags to temporary markers
238
         * so they don't accidentally break the string out of PHP,
239
         * and thus, thwart the highlighting.
240
         */
241
        $str = str_replace(
2✔
242
            ['&lt;', '&gt;', '<?', '?>', '<%', '%>', '\\', '</script>'],
2✔
243
            ['<', '>', 'phptagopen', 'phptagclose', 'asptagopen', 'asptagclose', 'backslashtmp', 'scriptclose'],
2✔
244
            $str
2✔
245
        );
2✔
246

247
        // The highlight_string function requires that the text be surrounded
248
        // by PHP tags, which we will remove later
249
        $str = highlight_string('<?php ' . $str . ' ?>', true);
2✔
250

251
        // Remove our artificially added PHP, and the syntax highlighting that came with it
252
        $str = preg_replace(
2✔
253
            [
2✔
254
                '/<span style="color: #([A-Z0-9]+)">&lt;\?php(&nbsp;| )/i',
2✔
255
                '/(<span style="color: #[A-Z0-9]+">.*?)\?&gt;<\/span>\n<\/span>\n<\/code>/is',
2✔
256
                '/<span style="color: #[A-Z0-9]+"\><\/span>/i',
2✔
257
            ],
2✔
258
            [
2✔
259
                '<span style="color: #$1">',
2✔
260
                "$1</span>\n</span>\n</code>",
2✔
261
                '',
2✔
262
            ],
2✔
263
            $str
2✔
264
        );
2✔
265

266
        // Replace our markers back to PHP tags.
267
        return str_replace(
2✔
268
            [
2✔
269
                'phptagopen',
2✔
270
                'phptagclose',
2✔
271
                'asptagopen',
2✔
272
                'asptagclose',
2✔
273
                'backslashtmp',
2✔
274
                'scriptclose',
2✔
275
            ],
2✔
276
            [
2✔
277
                '&lt;?',
2✔
278
                '?&gt;',
2✔
279
                '&lt;%',
2✔
280
                '%&gt;',
2✔
281
                '\\',
2✔
282
                '&lt;/script&gt;',
2✔
283
            ],
2✔
284
            $str
2✔
285
        );
2✔
286
    }
287
}
288

289
if (! function_exists('highlight_phrase')) {
1✔
290
    /**
291
     * Phrase Highlighter
292
     *
293
     * Highlights a phrase within a text string
294
     *
295
     * @param string $str      the text string
296
     * @param string $phrase   the phrase you'd like to highlight
297
     * @param string $tagOpen  the opening tag to precede the phrase with
298
     * @param string $tagClose the closing tag to end the phrase with
299
     */
300
    function highlight_phrase(string $str, string $phrase, string $tagOpen = '<mark>', string $tagClose = '</mark>'): string
301
    {
302
        return ($str !== '' && $phrase !== '') ? preg_replace('/(' . preg_quote($phrase, '/') . ')/i', $tagOpen . '\\1' . $tagClose, $str) : $str;
2✔
303
    }
304
}
305

306
if (! function_exists('convert_accented_characters')) {
1✔
307
    /**
308
     * Convert Accented Foreign Characters to ASCII
309
     *
310
     * @param string $str Input string
311
     */
312
    function convert_accented_characters(string $str): string
313
    {
314
        static $arrayFrom, $arrayTo;
3✔
315

316
        if (! is_array($arrayFrom)) {
3✔
317
            $config = new ForeignCharacters();
1✔
318

319
            if ($config->characterList === [] || ! is_array($config->characterList)) {
1✔
320
                $arrayFrom = [];
×
321
                $arrayTo   = [];
×
322

323
                return $str;
×
324
            }
325
            $arrayFrom = array_keys($config->characterList);
1✔
326
            $arrayTo   = array_values($config->characterList);
1✔
327

328
            unset($config);
1✔
329
        }
330

331
        return preg_replace($arrayFrom, $arrayTo, $str);
3✔
332
    }
333
}
334

335
if (! function_exists('word_wrap')) {
1✔
336
    /**
337
     * Word Wrap
338
     *
339
     * Wraps text at the specified character. Maintains the integrity of words.
340
     * Anything placed between {unwrap}{/unwrap} will not be word wrapped, nor
341
     * will URLs.
342
     *
343
     * @param string $str     the text string
344
     * @param int    $charlim = 76    the number of characters to wrap at
345
     */
346
    function word_wrap(string $str, int $charlim = 76): string
347
    {
348
        // Reduce multiple spaces
349
        $str = preg_replace('| +|', ' ', $str);
5✔
350

351
        // Standardize newlines
352
        if (str_contains($str, "\r")) {
5✔
353
            $str = str_replace(["\r\n", "\r"], "\n", $str);
1✔
354
        }
355

356
        // If the current word is surrounded by {unwrap} tags we'll
357
        // strip the entire chunk and replace it with a marker.
358
        $unwrap = [];
5✔
359

360
        if (preg_match_all('|\{unwrap\}(.+?)\{/unwrap\}|s', $str, $matches) >= 1) {
5✔
361
            for ($i = 0, $c = count($matches[0]); $i < $c; $i++) {
1✔
362
                $unwrap[] = $matches[1][$i];
1✔
363
                $str      = str_replace($matches[0][$i], '{{unwrapped' . $i . '}}', $str);
1✔
364
            }
365
        }
366

367
        // Use PHP's native function to do the initial wordwrap.
368
        // We set the cut flag to FALSE so that any individual words that are
369
        // too long get left alone. In the next step we'll deal with them.
370
        $str = wordwrap($str, $charlim, "\n", false);
5✔
371

372
        // Split the string into individual lines of text and cycle through them
373
        $output = '';
5✔
374

375
        foreach (explode("\n", $str) as $line) {
5✔
376
            // Is the line within the allowed character count?
377
            // If so we'll join it to the output and continue
378
            if (mb_strlen($line) <= $charlim) {
5✔
379
                $output .= $line . "\n";
5✔
380

381
                continue;
5✔
382
            }
383

384
            $temp = '';
2✔
385

386
            while (mb_strlen($line) > $charlim) {
2✔
387
                // If the over-length word is a URL we won't wrap it
388
                if (preg_match('!\[url.+\]|://|www\.!', $line)) {
2✔
389
                    break;
1✔
390
                }
391
                // Trim the word down
392
                $temp .= mb_substr($line, 0, $charlim - 1);
1✔
393
                $line = mb_substr($line, $charlim - 1);
1✔
394
            }
395

396
            // If $temp contains data it means we had to split up an over-length
397
            // word into smaller chunks so we'll add it back to our current line
398
            if ($temp !== '') {
2✔
399
                $output .= $temp . "\n" . $line . "\n";
1✔
400
            } else {
401
                $output .= $line . "\n";
1✔
402
            }
403
        }
404

405
        // Put our markers back
406
        foreach ($unwrap as $key => $val) {
5✔
407
            $output = str_replace('{{unwrapped' . $key . '}}', $val, $output);
1✔
408
        }
409

410
        // remove any trailing newline
411
        return rtrim($output);
5✔
412
    }
413
}
414

415
if (! function_exists('ellipsize')) {
1✔
416
    /**
417
     * Ellipsize String
418
     *
419
     * This function will strip tags from a string, split it at its max_length and ellipsize
420
     *
421
     * @param string    $str       String to ellipsize
422
     * @param int       $maxLength Max length of string
423
     * @param float|int $position  int (1|0) or float, .5, .2, etc for position to split
424
     * @param string    $ellipsis  ellipsis ; Default '...'
425
     *
426
     * @return string Ellipsized string
427
     */
428
    function ellipsize(string $str, int $maxLength, $position = 1, string $ellipsis = '&hellip;'): string
429
    {
430
        // Strip tags
431
        $str = trim(strip_tags($str));
1✔
432

433
        // Is the string long enough to ellipsize?
434
        if (mb_strlen($str) <= $maxLength) {
1✔
435
            return $str;
1✔
436
        }
437

438
        $beg      = mb_substr($str, 0, (int) floor($maxLength * $position));
1✔
439
        $position = ($position > 1) ? 1 : $position;
1✔
440

441
        if ($position === 1) {
1✔
442
            $end = mb_substr($str, 0, -($maxLength - mb_strlen($beg)));
1✔
443
        } else {
444
            $end = mb_substr($str, -($maxLength - mb_strlen($beg)));
1✔
445
        }
446

447
        return $beg . $ellipsis . $end;
1✔
448
    }
449
}
450

451
if (! function_exists('strip_slashes')) {
1✔
452
    /**
453
     * Strip Slashes
454
     *
455
     * Removes slashes contained in a string or in an array
456
     *
457
     * @param array|string $str string or array
458
     *
459
     * @return array|string string or array
460
     */
461
    function strip_slashes($str)
462
    {
463
        if (! is_array($str)) {
1✔
464
            return stripslashes($str);
1✔
465
        }
466

467
        foreach ($str as $key => $val) {
1✔
468
            $str[$key] = strip_slashes($val);
1✔
469
        }
470

471
        return $str;
1✔
472
    }
473
}
474

475
if (! function_exists('strip_quotes')) {
1✔
476
    /**
477
     * Strip Quotes
478
     *
479
     * Removes single and double quotes from a string
480
     */
481
    function strip_quotes(string $str): string
482
    {
483
        return str_replace(['"', "'"], '', $str);
1✔
484
    }
485
}
486

487
if (! function_exists('quotes_to_entities')) {
1✔
488
    /**
489
     * Quotes to Entities
490
     *
491
     * Converts single and double quotes to entities
492
     */
493
    function quotes_to_entities(string $str): string
494
    {
495
        return str_replace(["\\'", '"', "'", '"'], ['&#39;', '&quot;', '&#39;', '&quot;'], $str);
1✔
496
    }
497
}
498

499
if (! function_exists('reduce_double_slashes')) {
1✔
500
    /**
501
     * Reduce Double Slashes
502
     *
503
     * Converts double slashes in a string to a single slash,
504
     * except those found in http://
505
     *
506
     * http://www.some-site.com//index.php
507
     *
508
     * becomes:
509
     *
510
     * http://www.some-site.com/index.php
511
     */
512
    function reduce_double_slashes(string $str): string
513
    {
514
        return preg_replace('#(^|[^:])//+#', '\\1/', $str);
1✔
515
    }
516
}
517

518
if (! function_exists('reduce_multiples')) {
1✔
519
    /**
520
     * Reduce Multiples
521
     *
522
     * Reduces multiple instances of a particular character.  Example:
523
     *
524
     * Fred, Bill,, Joe, Jimmy
525
     *
526
     * becomes:
527
     *
528
     * Fred, Bill, Joe, Jimmy
529
     *
530
     * @param string $character the character you wish to reduce
531
     * @param bool   $trim      TRUE/FALSE - whether to trim the character from the beginning/end
532
     */
533
    function reduce_multiples(string $str, string $character = ',', bool $trim = false): string
534
    {
535
        $pattern = '#' . preg_quote($character, '#') . '{2,}#';
6✔
536
        $str     = preg_replace($pattern, $character, $str);
6✔
537

538
        return $trim ? trim($str, $character) : $str;
6✔
539
    }
540
}
541

542
if (! function_exists('random_string')) {
1✔
543
    /**
544
     * Create a Random String
545
     *
546
     * Useful for generating passwords or hashes.
547
     *
548
     * @param string $type Type of random string.  basic, alpha, alnum, numeric, nozero, md5, sha1, and crypto
549
     * @param int    $len  Number of characters
550
     *
551
     * @deprecated The type 'basic', 'md5', and 'sha1' are deprecated. They are not cryptographically secure.
552
     */
553
    function random_string(string $type = 'alnum', int $len = 8): string
554
    {
555
        switch ($type) {
556
            case 'alnum':
2✔
557
            case 'nozero':
2✔
558
            case 'alpha':
2✔
559
                switch ($type) {
560
                    case 'alpha':
1✔
561
                        $pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
1✔
562
                        break;
1✔
563

564
                    case 'alnum':
1✔
565
                        $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
1✔
566
                        break;
1✔
567

568
                    case 'nozero':
1✔
569
                        $pool = '123456789';
1✔
570
                        break;
1✔
571
                }
572

573
                return _from_random($len, $pool);
1✔
574

575
            case 'numeric':
2✔
576
                $max  = 10 ** $len - 1;
1✔
577
                $rand = random_int(0, $max);
1✔
578

579
                return sprintf('%0' . $len . 'd', $rand);
1✔
580

581
            case 'md5':
2✔
582
                return md5(uniqid((string) mt_rand(), true));
1✔
583

584
            case 'sha1':
2✔
585
                return sha1(uniqid((string) mt_rand(), true));
1✔
586

587
            case 'crypto':
2✔
588
                if ($len % 2 !== 0) {
2✔
589
                    throw new InvalidArgumentException(
1✔
590
                        'You must set an even number to the second parameter when you use `crypto`.'
1✔
591
                    );
1✔
592
                }
593

594
                return bin2hex(random_bytes($len / 2));
1✔
595
        }
596

597
        // 'basic' type treated as default
598
        return (string) mt_rand();
1✔
599
    }
600
}
601

602
if (! function_exists('_from_random')) {
1✔
603
    /**
604
     * The following function was derived from code of Symfony (v6.2.7 - 2023-02-28)
605
     * https://github.com/symfony/symfony/blob/80cac46a31d4561804c17d101591a4f59e6db3a2/src/Symfony/Component/String/ByteString.php#L45
606
     * Code subject to the MIT license (https://github.com/symfony/symfony/blob/v6.2.7/LICENSE).
607
     * Copyright (c) 2004-present Fabien Potencier
608
     *
609
     * The following method was derived from code of the Hack Standard Library (v4.40 - 2020-05-03)
610
     * https://github.com/hhvm/hsl/blob/80a42c02f036f72a42f0415e80d6b847f4bf62d5/src/random/private.php#L16
611
     * Code subject to the MIT license (https://github.com/hhvm/hsl/blob/master/LICENSE).
612
     * Copyright (c) 2004-2020, Facebook, Inc. (https://www.facebook.com/)
613
     *
614
     * @internal Outside the framework this should not be used directly.
615
     */
616
    function _from_random(int $length, string $pool): string
617
    {
618
        if ($length <= 0) {
1✔
619
            throw new InvalidArgumentException(
×
620
                sprintf('A strictly positive length is expected, "%d" given.', $length)
×
621
            );
×
622
        }
623

624
        $poolSize = \strlen($pool);
1✔
625
        $bits     = (int) ceil(log($poolSize, 2.0));
1✔
626
        if ($bits <= 0 || $bits > 56) {
1✔
627
            throw new InvalidArgumentException(
×
628
                'The length of the alphabet must in the [2^1, 2^56] range.'
×
629
            );
×
630
        }
631

632
        $string = '';
1✔
633

634
        while ($length > 0) {
1✔
635
            $urandomLength = (int) ceil(2 * $length * $bits / 8.0);
1✔
636
            $data          = random_bytes($urandomLength);
1✔
637
            $unpackedData  = 0;
1✔
638
            $unpackedBits  = 0;
1✔
639

640
            for ($i = 0; $i < $urandomLength && $length > 0; $i++) {
1✔
641
                // Unpack 8 bits
642
                $unpackedData = ($unpackedData << 8) | \ord($data[$i]);
1✔
643
                $unpackedBits += 8;
1✔
644

645
                // While we have enough bits to select a character from the alphabet, keep
646
                // consuming the random data
647
                for (; $unpackedBits >= $bits && $length > 0; $unpackedBits -= $bits) {
1✔
648
                    $index = ($unpackedData & ((1 << $bits) - 1));
1✔
649
                    $unpackedData >>= $bits;
1✔
650
                    // Unfortunately, the alphabet size is not necessarily a power of two.
651
                    // Worst case, it is 2^k + 1, which means we need (k+1) bits and we
652
                    // have around a 50% chance of missing as k gets larger
653
                    if ($index < $poolSize) {
1✔
654
                        $string .= $pool[$index];
1✔
655
                        $length--;
1✔
656
                    }
657
                }
658
            }
659
        }
660

661
        return $string;
1✔
662
    }
663
}
664

665
if (! function_exists('increment_string')) {
1✔
666
    /**
667
     * Add's _1 to a string or increment the ending number to allow _2, _3, etc
668
     *
669
     * @param string $str       Required
670
     * @param string $separator What should the duplicate number be appended with
671
     * @param int    $first     Which number should be used for the first dupe increment
672
     */
673
    function increment_string(string $str, string $separator = '_', int $first = 1): string
674
    {
675
        preg_match('/(.+)' . preg_quote($separator, '/') . '([0-9]+)$/', $str, $match);
1✔
676

677
        return isset($match[2]) ? $match[1] . $separator . ((int) $match[2] + 1) : $str . $separator . $first;
1✔
678
    }
679
}
680

681
if (! function_exists('alternator')) {
1✔
682
    /**
683
     * Alternator
684
     *
685
     * Allows strings to be alternated. See docs...
686
     *
687
     * @param string ...$args (as many parameters as needed)
688
     */
689
    function alternator(...$args): string
690
    {
691
        static $i;
2✔
692

693
        if (func_num_args() === 0) {
2✔
694
            $i = 0;
2✔
695

696
            return '';
2✔
697
        }
698

699
        return $args[($i++ % count($args))];
1✔
700
    }
701
}
702

703
if (! function_exists('excerpt')) {
1✔
704
    /**
705
     * Excerpt.
706
     *
707
     * Allows to extract a piece of text surrounding a word or phrase.
708
     *
709
     * @param string $text     String to search the phrase
710
     * @param string $phrase   Phrase that will be searched for.
711
     * @param int    $radius   The amount of characters returned around the phrase.
712
     * @param string $ellipsis Ending that will be appended
713
     *
714
     * If no $phrase is passed, will generate an excerpt of $radius characters
715
     * from the beginning of $text.
716
     */
717
    function excerpt(string $text, ?string $phrase = null, int $radius = 100, string $ellipsis = '...'): string
718
    {
719
        if (isset($phrase)) {
3✔
720
            $phrasePosition = mb_stripos($text, $phrase);
2✔
721
            $phraseLength   = mb_strlen($phrase);
2✔
722
        } else {
723
            $phrasePosition = $radius / 2;
1✔
724
            $phraseLength   = 1;
1✔
725
        }
726

727
        $beforeWords = explode(' ', mb_substr($text, 0, $phrasePosition));
3✔
728
        $afterWords  = explode(' ', mb_substr($text, $phrasePosition + $phraseLength));
3✔
729

730
        $firstPartOutput = ' ';
3✔
731
        $endPartOutput   = ' ';
3✔
732
        $count           = 0;
3✔
733

734
        foreach (array_reverse($beforeWords) as $beforeWord) {
3✔
735
            $beforeWordLength = mb_strlen($beforeWord);
3✔
736

737
            if (($beforeWordLength + $count + 1) < $radius) {
3✔
738
                $firstPartOutput = ' ' . $beforeWord . $firstPartOutput;
3✔
739
            }
740

741
            $count = ++$count + $beforeWordLength;
3✔
742
        }
743

744
        $count = 0;
3✔
745

746
        foreach ($afterWords as $afterWord) {
3✔
747
            $afterWordLength = mb_strlen($afterWord);
3✔
748

749
            if (($afterWordLength + $count + 1) < $radius) {
3✔
750
                $endPartOutput .= $afterWord . ' ';
3✔
751
            }
752

753
            $count = ++$count + $afterWordLength;
3✔
754
        }
755

756
        $ellPre = $phrase !== null ? $ellipsis : '';
3✔
757

758
        return str_replace('  ', ' ', $ellPre . $firstPartOutput . $phrase . $endPartOutput . $ellipsis);
3✔
759
    }
760
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc