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

Cecilapp / Cecil / 26414009793

25 May 2026 06:15PM UTC coverage: 82.54%. First build
26414009793

Pull #2383

github

web-flow
Merge e6b723fa2 into 4904a42be
Pull Request #2383: refactor: asset handling and add renderer extensions

282 of 358 new or added lines in 10 files covered. (78.77%)

3503 of 4244 relevant lines covered (82.54%)

0.83 hits per line

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

91.3
/src/Util/Slugifier.php
1
<?php
2

3
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <arnaud@ligny.fr>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types=1);
13

14
namespace Cecil\Util;
15

16
use Cecil\Exception\RuntimeException;
17
use Symfony\Component\String\Slugger\AsciiSlugger;
18

19
/**
20
 * Converts arbitrary paths/strings into URI-safe slugs.
21
 *
22
 * Preserves '.', '_', and '/' characters, handles non-ASCII (including CJK)
23
 * via the Symfony AsciiSlugger, and replaces any remaining unsafe characters
24
 * with dashes.
25
 */
26
class Slugifier
27
{
28
    /** @see https://regex101.com/r/... */
29
    public const SLUGIFY_PATTERN = '/(^\/|[^._a-z0-9\/]|-)+/';
30

31
    /** @var AsciiSlugger|null */
32
    private static $slugifier;
33

34
    /**
35
     * Turns a path (string) into a slug (URI).
36
     *
37
     * @throws RuntimeException
38
     */
39
    public static function slugify(string $path): string
40
    {
41
        if (!self::$slugifier instanceof AsciiSlugger) {
1✔
42
            self::$slugifier = new AsciiSlugger();
1✔
43
        }
44

45
        $placeholders = self::createSlugifyPlaceholders($path);
1✔
46
        $path = strtr($path, $placeholders);
1✔
47

48
        $path = preg_replace_callback('/[^\x00-\x7F]+/u', static function (array $matches): string {
1✔
49
            $locale = preg_match('/\p{Han}/u', $matches[0]) ? 'zh' : null;
1✔
50

51
            return self::$slugifier->slug($matches[0], '-', $locale)->lower()->toString();
1✔
52
        }, $path);
1✔
53
        if ($path === null) {
1✔
NEW
54
            throw new RuntimeException('Unable to slugify path.');
×
55
        }
56

57
        $path = preg_replace(self::SLUGIFY_PATTERN, '-', strtolower($path));
1✔
58
        if ($path === null) {
1✔
NEW
59
            throw new RuntimeException('Unable to slugify path.');
×
60
        }
61

62
        return ltrim(trim(strtr($path, array_flip($placeholders)), '-'), '/');
1✔
63
    }
64

65
    private static function createSlugifyPlaceholders(string $path): array
66
    {
67
        $placeholders = [];
1✔
68

69
        foreach (['.' => 'dot', '_' => 'underscore', '/' => 'slash'] as $character => $name) {
1✔
70
            $placeholders[$character] = self::createSlugifyPlaceholder($path, $name);
1✔
71
        }
72

73
        return $placeholders;
1✔
74
    }
75

76
    private static function createSlugifyPlaceholder(string $path, string $name): string
77
    {
78
        $index = 0;
1✔
79

80
        do {
81
            $placeholder = \sprintf('cecil%s%s', $name, substr(hash('sha256', $path . $name . $index), 0, 16));
1✔
82
            ++$index;
1✔
83
        } while (str_contains($path, $placeholder));
1✔
84

85
        return $placeholder;
1✔
86
    }
87
}
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