@kirigami/php-prepros 1.1.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -98,45 +98,45 @@ class SCRAPER
98
98
  }
99
99
  }
100
100
 
101
- if(($results = $xpath->query('//meta[@name="dc.title"]')) && $results->length) $metas->title = getFirstItemAttr($results, 'content');
102
- if(($results = $xpath->query('//meta[@name="dcterms.title"]')) && $results->length) $metas->title = getFirstItemAttr($results, 'content');
103
- if(($results = $xpath->query('//meta[@name="twitter:title"]')) && $results->length) $metas->title = getFirstItemAttr($results, 'content');
104
- if(($results = $xpath->query('//meta[@property="og:title"]')) && $results->length) $metas->title = getFirstItemAttr($results, 'content');
105
- if(($results = $xpath->query('//shreddit-title')) && $results->length) $metas->title = getFirstItemAttr($results, 'title');
106
- if(($results = $xpath->query('//meta[@name="dc.description"]')) && $results->length) $metas->description = getFirstItemAttr($results, 'content');
107
- if(($results = $xpath->query('//meta[@name="dcterms.description"]')) && $results->length) $metas->description = getFirstItemAttr($results, 'content');
108
- if(($results = $xpath->query('//meta[@name="twitter:description"]')) && $results->length) $metas->description = getFirstItemAttr($results, 'content');
109
- if(($results = $xpath->query('//meta[@property="og:description"]')) && $results->length) $metas->description = getFirstItemAttr($results, 'content');
110
- if(($results = $xpath->query('//meta[@name="description"]')) && $results->length) $metas->description = getFirstItemAttr($results, 'content');
111
- if(($results = $xpath->query('//meta[@name="twitter:image"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'content');
112
- if(($results = $xpath->query('//meta[@property="og:image"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'content');
113
- if(($results = $xpath->query('//meta[@name="og:image"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'content');
114
- if(($results = $xpath->query('//img[@rel="og:image rdfs:seeAlso"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'src');
115
- if(($results = $xpath->query('//link[@rel="canonical"]')) && $results->length) $metas->url = getFirstItemAttr($results, 'href');
116
- if(($results = $xpath->query('//meta[@name="twitter:url"]')) && $results->length) $metas->url = getFirstItemAttr($results, 'content');
117
- if(($results = $xpath->query('//meta[@property="og:url"]')) && $results->length) $metas->url = getFirstItemAttr($results, 'content');
118
-
119
- if(empty($metas->label) && ($results = $xpath->query('//meta[@name="twitter:app:name:iphone"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
120
- if(empty($metas->label) && ($results = $xpath->query('//meta[@name="twitter:app:name:googleplay"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
121
- if(empty($metas->label) && ($results = $xpath->query('//meta[@name="og:site_name"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
122
- if(empty($metas->label) && ($results = $xpath->query('//meta[@property="og:site_name"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
123
- if(empty($metas->label) && ($results = $xpath->query('//meta[@name="dc.publisher"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
124
- if(empty($metas->label) && ($results = $xpath->query('//meta[@name="DC.Publisher"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
125
- if(empty($metas->label) && ($results = $xpath->query('//meta[@name="publisher"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
126
- if(empty($metas->label) && ($results = $xpath->query('//meta[@property="article:author"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
127
- if(empty($metas->label) && ($results = $xpath->query('//meta[@name="author"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
128
- if(empty($metas->label) && ($results = $xpath->query('//meta[@name="twitter:creator"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
129
- if(empty($metas->label) && ($results = $xpath->query('//meta[@name="dcterms.creator"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
130
- if(empty($metas->label) && ($results = $xpath->query('//meta[@property="al:ios:app_name"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
131
- if(empty($metas->label) && ($results = $xpath->query('//meta[@property="al:android:app_name"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
132
- if(empty($metas->label) && ($results = $xpath->query('//meta[@name="DC.publisher"]')) && $results->length) $metas->label = getFirstItemAttr($results, 'content');
101
+ if(($results = $xpath->query('//meta[@name="dc.title"]')) && $results->length) $metas->title = self::getFirstItemAttr($results, 'content');
102
+ if(($results = $xpath->query('//meta[@name="dcterms.title"]')) && $results->length) $metas->title = self::getFirstItemAttr($results, 'content');
103
+ if(($results = $xpath->query('//meta[@name="twitter:title"]')) && $results->length) $metas->title = self::getFirstItemAttr($results, 'content');
104
+ if(($results = $xpath->query('//meta[@property="og:title"]')) && $results->length) $metas->title = self::getFirstItemAttr($results, 'content');
105
+ if(($results = $xpath->query('//shreddit-title')) && $results->length) $metas->title = self::getFirstItemAttr($results, 'title');
106
+ if(($results = $xpath->query('//meta[@name="dc.description"]')) && $results->length) $metas->description = self::getFirstItemAttr($results, 'content');
107
+ if(($results = $xpath->query('//meta[@name="dcterms.description"]')) && $results->length) $metas->description = self::getFirstItemAttr($results, 'content');
108
+ if(($results = $xpath->query('//meta[@name="twitter:description"]')) && $results->length) $metas->description = self::getFirstItemAttr($results, 'content');
109
+ if(($results = $xpath->query('//meta[@property="og:description"]')) && $results->length) $metas->description = self::getFirstItemAttr($results, 'content');
110
+ if(($results = $xpath->query('//meta[@name="description"]')) && $results->length) $metas->description = self::getFirstItemAttr($results, 'content');
111
+ if(($results = $xpath->query('//meta[@name="twitter:image"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'content');
112
+ if(($results = $xpath->query('//meta[@property="og:image"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'content');
113
+ if(($results = $xpath->query('//meta[@name="og:image"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'content');
114
+ if(($results = $xpath->query('//img[@rel="og:image rdfs:seeAlso"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'src');
115
+ if(($results = $xpath->query('//link[@rel="canonical"]')) && $results->length) $metas->url = self::getFirstItemAttr($results, 'href');
116
+ if(($results = $xpath->query('//meta[@name="twitter:url"]')) && $results->length) $metas->url = self::getFirstItemAttr($results, 'content');
117
+ if(($results = $xpath->query('//meta[@property="og:url"]')) && $results->length) $metas->url = self::getFirstItemAttr($results, 'content');
118
+
119
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@name="twitter:app:name:iphone"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
120
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@name="twitter:app:name:googleplay"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
121
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@name="og:site_name"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
122
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@property="og:site_name"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
123
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@name="dc.publisher"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
124
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@name="DC.Publisher"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
125
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@name="publisher"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
126
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@property="article:author"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
127
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@name="author"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
128
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@name="twitter:creator"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
129
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@name="dcterms.creator"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
130
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@property="al:ios:app_name"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
131
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@property="al:android:app_name"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
132
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@name="DC.publisher"]')) && $results->length) $metas->label = self::getFirstItemAttr($results, 'content');
133
133
  if(empty($metas->label) && ($results = $xpath->query('//span[@class="footer__signature"]')) && $results->length) $metas->label = $results->item(0)->textContent;
134
134
  if(empty($metas->label) && ($results = $xpath->query('//p[@class="site-title"]')) && $results->length) $metas->label = trim($results->item(0)->textContent);
135
- if(empty($metas->label) && ($results = $xpath->query('//meta[@name="copyright"]')) && $results->length) $metas->label = trim(getFirstItemAttr($results, 'content'), '© ');
135
+ if(empty($metas->label) && ($results = $xpath->query('//meta[@name="copyright"]')) && $results->length) $metas->label = trim(self::getFirstItemAttr($results, 'content'), '© ');
136
136
  if(empty($metas->label) && preg_match("#\{'blogId': '[0-9]+', 'title': '([^']+)'#i", $contents, $m)) $metas->label = $m[1];
137
137
 
138
138
  if(($results = $xpath->query('//link[@rel="alternate" and @type="application/json+oembed" and @href]')) && $results->length) {
139
- if(STR::is_url(($oembedurl = getFirstItemAttr($results, 'href')))) {
139
+ if(STR::is_url(($oembedurl = self::getFirstItemAttr($results, 'href')))) {
140
140
  if($json = self::getContents($oembedurl)) {
141
141
  if($oem = json_decode($json)) {
142
142
  if(empty($metas->title) && !empty($oem->title)) $metas->title = $oem->title;
@@ -171,7 +171,7 @@ class SCRAPER
171
171
  }
172
172
  if(empty($metas->label)) {
173
173
  if(($results = $xpath->query('//link[@rel="manifest" and @href]')) && $results->length) {
174
- if($manifesturl = get_absolute_url($url, getFirstItemAttr($results, 'href'))) {
174
+ if($manifesturl = get_absolute_url($url, self::getFirstItemAttr($results, 'href'))) {
175
175
  if($json = self::getContents($manifesturl)) {
176
176
  if($manifest = json_decode($json)) {
177
177
  if(!empty($manifest->name)) $metas->label = $manifest->name;
@@ -181,10 +181,10 @@ class SCRAPER
181
181
  }
182
182
  }
183
183
  if(empty($metas->label) && (($results = $xpath->query('//meta[@property="fb:page_id"]')) && $results->length)) {
184
- if($_contents = self::getContents('https://facebook.com/' . getFirstItemAttr($results, 'content'))) {
184
+ if($_contents = self::getContents('https://facebook.com/' . self::getFirstItemAttr($results, 'content'))) {
185
185
  if($xp = self::loadHTML($_contents)) {
186
186
  if(($results = $xp->query('//meta[@property="og:title"]')) && $results->length) {
187
- $metas->label = getFirstItemAttr($results, 'content');
187
+ $metas->label = self::getFirstItemAttr($results, 'content');
188
188
  }
189
189
  }
190
190
  }
@@ -196,14 +196,14 @@ class SCRAPER
196
196
 
197
197
 
198
198
  if(empty($metas->image) && ($results = $xpath->query('//span[@class="background-image not-mobile"]')) && $results->length)
199
- if(preg_match('#url\(\'(.*?)\'\)#i', getFirstItemAttr($results, 'style'), $m))
199
+ if(preg_match('#url\(\'(.*?)\'\)#i', self::getFirstItemAttr($results, 'style'), $m))
200
200
  $metas->image = $m[1];
201
201
 
202
- if(empty($metas->image) && ($results = $xpath->query('//link[@rel="icon" and @type="image/jpeg"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'href');
203
- if(empty($metas->image) && ($results = $xpath->query('//link[@rel="icon" and @type="image/png"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'href');
204
- if(empty($metas->image) && ($results = $xpath->query('//article//img')) && $results->length) $metas->image = getFirstItemAttr($results, 'src');
205
- if(empty($metas->image) && ($results = $xpath->query('//div[@id="image_box"]//img')) && $results->length) $metas->image = getFirstItemAttr($results, 'src');
206
- if(empty($metas->image) && ($results = $xpath->query('//link[@rel="shortcut icon" and @type="image/jpeg" and @href]')) && $results->length) $metas->image = getFirstItemAttr($results, 'href');
202
+ if(empty($metas->image) && ($results = $xpath->query('//link[@rel="icon" and @type="image/jpeg"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'href');
203
+ if(empty($metas->image) && ($results = $xpath->query('//link[@rel="icon" and @type="image/png"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'href');
204
+ if(empty($metas->image) && ($results = $xpath->query('//article//img')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'src');
205
+ if(empty($metas->image) && ($results = $xpath->query('//div[@id="image_box"]//img')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'src');
206
+ if(empty($metas->image) && ($results = $xpath->query('//link[@rel="shortcut icon" and @type="image/jpeg" and @href]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'href');
207
207
  if(empty($metas->image) && ($results = $xpath->query('//link[@rel="apple-touch-icon" and @sizes]')) && $results->length) {
208
208
  $size = 0;
209
209
  foreach($results as $item) {
@@ -215,14 +215,14 @@ class SCRAPER
215
215
  }
216
216
  }
217
217
  }
218
- if(empty($metas->image) && ($results = $xpath->query('//link[@rel="apple-touch-icon"]')) && $results->length) $metas->image = getFirstItemAttr($results, 'href');
218
+ if(empty($metas->image) && ($results = $xpath->query('//link[@rel="apple-touch-icon"]')) && $results->length) $metas->image = self::getFirstItemAttr($results, 'href');
219
219
  if(empty($metas->title) && ($results = $xpath->query('//title')) && $results->length) $metas->title = $results->item(0)->textContent;
220
220
  if(empty($metas->label)) { $parts = explode('|', $metas->title); if(count($parts) > 1) $metas->label = trim(array_pop($parts)); }
221
221
  if(empty($metas->label)) { $parts = explode(' - ', $metas->title); if(count($parts) > 1) $metas->label = trim(array_pop($parts)); }
222
222
  if(strpos($metas->label, '@') === 0) $metas->label = ucwords(preg_replace('#^@#i', '', $metas->label));
223
223
 
224
224
  if(empty($metas->label) && ($results = $xpath->query('//div[@id="elevate-global" and @data-elevate-global]')) && $results->length)
225
- if($data = json_decode(getFirstItemAttr($results, 'data-elevate-global')))
225
+ if($data = json_decode(self::getFirstItemAttr($results, 'data-elevate-global')))
226
226
  if(!empty($data->content_organization))
227
227
  $metas->label = $data->content_organization;
228
228
 
@@ -248,9 +248,9 @@ class SCRAPER
248
248
  if($_contents = self::getContents(str_replace('\\/', '/', $m[1])))
249
249
  if($xp = self::loadHTML($_contents))
250
250
  if(($results = $xp->query('//link[@rel="preload" and @as="image" and contains(@href, ".jpg")]')) && $results->length)
251
- $metas->image = getFirstItemAttr($results, 'href');
251
+ $metas->image = self::getFirstItemAttr($results, 'href');
252
252
 
253
- if(!url_exists($metas->image, '^image/') && ($results = $xpath->query('//img[@src]')) && $results->length) {
253
+ if(!self::url_exists($metas->image, '^image/') && ($results = $xpath->query('//img[@src]')) && $results->length) {
254
254
  foreach($results as $item) {
255
255
  /** @var DOMElement $item */
256
256
  if(in_array(pathinfo(parse_url($item->getAttribute('src'), PHP_URL_PATH), PATHINFO_EXTENSION), ['jpg', 'jpeg', 'png', 'webp'])) {
@@ -301,20 +301,25 @@ class SCRAPER
301
301
  return $metas;
302
302
  }
303
303
 
304
- }
305
304
 
305
+ private static function getFirstItemAttr(\Dom\Node|\Dom\NodeList $results, string $attr) {
306
+ /** @var DOM\Element $item */
307
+ $item = $results->item(0);
308
+ return $item->getAttribute($attr);
309
+ }
310
+
311
+
312
+ private static function url_exists(string $url, string|null $mimereg = null) {
313
+ if(($exists = CACHE::get(($key = 'urlexists_' . STR::shorthash($url)))) !== null) return $exists->response;
314
+ $exists = new stdClass;
315
+ $exists->response = CURL::urlExists($url, $mimereg);
316
+ CACHE::set($key, $exists);
317
+ return $exists->response;
318
+ }
306
319
 
307
- function getFirstItemAttr(\Dom\Node|\Dom\NodeList $results, string $attr) {
308
- /** @var DOM\Element $item */
309
- $item = $results->item(0);
310
- return $item->getAttribute($attr);
311
320
  }
312
321
 
313
322
 
314
- function url_exists(string $url, string|null $mimereg = null) {
315
- if(($exists = CACHE::get(($key = 'urlexists_' . STR::shorthash($url)))) !== null) return $exists->response;
316
- $exists = new stdClass;
317
- $exists->response = CURL::urlExists($url, $mimereg);
318
- CACHE::set($key, $exists);
319
- return $exists->response;
320
- }
323
+
324
+
325
+
@@ -24,6 +24,9 @@ class STD {
24
24
  $return->files = PREPROS::getExportedFiles();
25
25
  if(is_string($props)) $return->error = $props;
26
26
  else foreach($props as $k => $v) $return->{$k} = $v;
27
+ // Consumers read `->error`; keep it populated whatever key the caller
28
+ // used (some pass `message`) so nothing ever surfaces as "undefined".
29
+ if(empty($return->error)) $return->error = $return->message ?? 'Unknown error.';
27
30
  file_put_contents(self::RESULT_PATH, json_encode($return, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), FILE_APPEND);
28
31
  exit(1);
29
32
  }
@@ -12,21 +12,51 @@ class STR
12
12
 
13
13
  public static function replaceTags(string $tag, string $contents, callable $clb): string
14
14
  {
15
- $contents = preg_replace_callback('#<' . preg_quote($tag, '#') . '([^>]*)>(.*?)</' . preg_quote($tag, '#') . '>#msi', function ($m) use ($clb) {
16
- return call_user_func($clb, $m[0], self::parseHtmlAttributes($m[1]), $m[2]);
17
- }, $contents);
18
- return $contents;
15
+ $t = preg_quote($tag, '#');
16
+
17
+ $pattern = '#<' . $t . '([^>]*)>(.*?)</' . $t . '>'
18
+ . '|<' . $t . '([^>]*)/>'
19
+ . '|<' . $t . '([^>]*)>#msi';
20
+
21
+ return preg_replace_callback($pattern, function ($m) use ($clb) {
22
+ if (isset($m[1]) || isset($m[2])) {
23
+ // paired form: <tag>content</tag>
24
+ $attrs = $m[1] ?? '';
25
+ $inner = $m[2] ?? '';
26
+ } elseif (isset($m[3])) {
27
+ // self-closing: <tag ... />
28
+ $attrs = $m[3];
29
+ $inner = '';
30
+ } else {
31
+ // opening only, no closing (img, meta, br, ...)
32
+ $attrs = $m[4] ?? '';
33
+ $inner = '';
34
+ }
35
+ return call_user_func($clb, $m[0], self::parseHtmlAttributes($attrs), $inner);
36
+ }, $contents, -1, $count, PREG_UNMATCHED_AS_NULL); // <-- the flag that fixes everything
19
37
  }
20
38
 
21
39
 
22
40
  public static function parseHtmlAttributes(string $attributes): array
23
41
  {
24
- if (preg_match_all('#(\\w+)\s*=\\s*("[^"]*"|\'[^\']*\'|[^"\'\\s>]*)#i', $attributes, $m)) {
25
- foreach ($m[1] as $k => $key) {
26
- $attrs[strtolower($key)] = stripslashes(substr($m[2][$k], 1, -1));;
42
+ $attrs = [];
43
+ if (preg_match_all('#(\w+)(?:\s*=\s*("[^"]*"|\'[^\']*\'|[^"\'\s>]*))?#i', $attributes, $m, PREG_SET_ORDER)) {
44
+ foreach ($m as $match) {
45
+ $key = strtolower($match[1]);
46
+ if (isset($match[2]) && $match[2] !== '') {
47
+ $value = $match[2];
48
+ // strip the quotes only if they're present
49
+ if (($value[0] === '"' || $value[0] === "'") && $value[0] === substr($value, -1)) {
50
+ $value = substr($value, 1, -1);
51
+ }
52
+ $attrs[$key] = stripslashes($value);
53
+ } else {
54
+ // boolean attribute: selected, muted, disabled, checked...
55
+ $attrs[$key] = true;
56
+ }
27
57
  }
28
58
  }
29
- return isset($attrs) ? $attrs : [];
59
+ return $attrs;
30
60
  }
31
61
 
32
62
 
@@ -48,33 +78,37 @@ class STR
48
78
 
49
79
  public static function is_url(string $str): bool
50
80
  {
51
- if (!$sheme = strtolower(@parse_url($str, PHP_URL_SCHEME))) return false;
81
+ if (!$sheme = strtolower((string) @parse_url($str, PHP_URL_SCHEME))) return false;
52
82
  return in_array($sheme, ['http', 'https', 'itunes', 'ftp', 'ftps', 'ssh', 'ssl', 'sftp']);
53
83
  }
54
84
 
55
85
 
56
86
  public static function html_entities_decode(string $str): string
57
87
  {
58
- // $str = html_entity_decode(trim($str), ENT_QUOTES, 'UTF-8');
59
- // $str = htmlentities($str, ENT_QUOTES, 'UTF-8');
60
- // $str = mb_convert_encoding($str, 'HTML-ENTITIES', 'UTF-8');
61
88
  $str = html_entity_decode(trim($str), ENT_QUOTES, 'UTF-8');
62
- // $str = htmlentities($str, ENT_QUOTES, 'UTF-8');
63
89
  return $str;
64
90
  }
65
91
 
66
92
 
67
- public static function shorthash(string$str): string
93
+ public static function shorthash(string $str): string
68
94
  {
69
95
  return substr(hash('sha256', $str), 0, 12);
70
96
  }
71
97
 
72
98
 
73
- public static function slug(string $str): string
99
+ public static function normalize(string $str) {
100
+ $str = Normalizer::normalize($str, Normalizer::FORM_D);
101
+ $str = preg_replace('/\p{Mn}/u', '', $str);
102
+ return $str;
103
+ }
104
+
105
+
106
+ public static function slug(string $str, $sep = ''): string
74
107
  {
108
+ $str = self::normalize($str);
75
109
  $str = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str);
76
110
  $str = strtolower($str);
77
- $str = preg_replace('/[^a-z0-9]/', '', $str);
111
+ $str = preg_replace('/[^a-z0-9]/', $sep, $str);
78
112
  return $str;
79
113
  }
80
114