Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
TitleSlugifier
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
2 / 2
3
100.00% covered (success)
100.00%
1 / 1
 slug
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 replacePattern
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * Extracts SEO slug generation from Link to reduce complexity.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2025 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2025-11-08
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Link\Util;
21
22use phpMyFAQ\Strings;
23
24final class TitleSlugifier
25{
26    private const string REGEX_MULTI_DASH = '/-{2,}/m';
27
28    private const string REGEX_INNER_DASH = '/(?<=\w)-(?=\w)/m';
29
30    private const string REGEX_WHITESPACE = '/\s+/m';
31
32    private const array PUNCTUATION = [
33        '+',
34        ',',
35        ';',
36        ':',
37        '.',
38        '?',
39        '!',
40        '"',
41        '(',
42        ')',
43        '[',
44        ']',
45        '{',
46        '}',
47        '<',
48        '>',
49        '%',
50    ];
51
52    private const array UMLAUTS = [
53        'à',
54        'è',
55        'é',
56        'ì',
57        'ò',
58        'ù',
59        'ä',
60        'ö',
61        'ü',
62        'ß',
63        'Ä',
64        'Ö',
65        'Ü',
66        'č',
67        'ę',
68        'ė',
69        'į',
70        'š',
71        'ų',
72        'ū',
73        'ž',
74    ];
75
76    private const array UMLAUTS_REPLACEMENTS = [
77        'a',
78        'e',
79        'e',
80        'i',
81        'o',
82        'u',
83        'ae',
84        'oe',
85        'ue',
86        'ss',
87        'Ae',
88        'Oe',
89        'Ue',
90        'c',
91        'e',
92        'e',
93        'i',
94        's',
95        'u',
96        'u',
97        'z',
98    ];
99
100    public static function slug(string $title): string
101    {
102        $itemTitle = trim($title);
103        $itemTitle = Strings::strtolower($itemTitle);
104        // replace apostrophe and slash with underscore
105        $itemTitle = str_replace(["'", '/', '&#39;', '&#039;'], replace: '_', subject: $itemTitle);
106        // collapse multiple dashes to a space (will become '-')
107        $itemTitle = self::replacePattern(self::REGEX_MULTI_DASH, ' ', $itemTitle);
108        // replace single inner dashes with underscores (legacy: HD-Ready => hd_ready)
109        $itemTitle = self::replacePattern(self::REGEX_INNER_DASH, '_', $itemTitle);
110        // whitespace to '-'
111        $itemTitle = self::replacePattern(self::REGEX_WHITESPACE, '-', $itemTitle);
112        // strip punctuation
113        $itemTitle = str_replace(self::PUNCTUATION, replace: '', subject: $itemTitle);
114        // map umlauts and accents
115        $itemTitle = str_replace(self::UMLAUTS, self::UMLAUTS_REPLACEMENTS, $itemTitle);
116        // reduce multiple separators
117        $itemTitle = self::replacePattern('/_{2,}/m', '_', $itemTitle);
118        $itemTitle = self::replacePattern(self::REGEX_MULTI_DASH, '-', $itemTitle);
119        // trim edge separators
120        return trim($itemTitle, characters: '-_');
121    }
122
123    /**
124     * Applies a regex replacement and keeps the input when the engine fails.
125     */
126    private static function replacePattern(string $pattern, string $replacement, string $subject): string
127    {
128        $replaced = Strings::preg_replace($pattern, $replacement, $subject);
129
130        return is_string($replaced) ? $replaced : $subject;
131    }
132}