ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
MakeClickable.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
21namespace ILIAS\Refinery\String;
22
27
29{
32
33 private const URL_PATTERN = '(^|[^[:alnum:]])(((https?:\/\/)|(www.))[^[:cntrl:][:space:]<>\'"]+)([^[:alnum:]]|$)';
34
35 private bool $open_in_new_tab;
36
37 public function __construct($open_in_new_tab = true)
38 {
39 $this->open_in_new_tab = $open_in_new_tab;
40 }
41
42 public function transform($from): string
43 {
44 $this->requireString($from);
45
46 return $this->replaceMatches($from, fn(int $startOfMatch, int $endOfMatch, string $url, string $protocol): string => (
47 $this->shouldReplace($from, $startOfMatch, $endOfMatch) ?
48 $this->replace($url, $protocol) :
49 $url
50 ));
51 }
52
53 private function replaceMatches(string $from, callable $replace): string
54 {
55 $endOfLastMatch = 0;
56 $stringParts = [];
57
58 while (null !== ($matches = $this->match(self::URL_PATTERN, substr($from, $endOfLastMatch)))) {
59 $startOfMatch = $endOfLastMatch + strpos(substr($from, $endOfLastMatch), $matches[0]);
60 $endOfMatch = $startOfMatch + strlen($matches[1] . $matches[2]);
61
62 $stringParts[] = substr($from, $endOfLastMatch, $startOfMatch - $endOfLastMatch);
63 $stringParts[] = $matches[1] . $replace($startOfMatch, $endOfMatch, $matches[2], $matches[4]);
64
65 $endOfLastMatch = $endOfMatch;
66 }
67
68 $stringParts[] = substr($from, $endOfLastMatch);
69
70 return implode('', $stringParts);
71 }
72
73 private function regexPos(string $regexp, string $string): int
74 {
75 $matches = $this->match($regexp, $string);
76 if (null !== $matches) {
77 return strpos($string, $matches[0]);
78 }
79
80 return strlen($string);
81 }
82
87 private function requireString($maybeHTML): void
88 {
89 if (!is_string($maybeHTML)) {
90 throw new ConstraintViolationException('not a string', 'not_a_string');
91 }
92 }
93
94 private function shouldReplace(string $maybeHTML, int $startOfMatch, int $endOfMatch): bool
95 {
96 $isNotInAnchor = $this->regexPos('<a.*</a>', substr($maybeHTML, $endOfMatch)) <= $this->regexPos('</a>', substr($maybeHTML, $endOfMatch));
97 $isNotATagAttribute = null === $this->match('^[^>]*[[:space:]][[:alpha:]]+<', strrev(substr($maybeHTML, 0, $startOfMatch)));
98
99 return $isNotInAnchor && $isNotATagAttribute;
100 }
101
106 private function match(string $pattern, string $haystack): ?array
107 {
108 $pattern = str_replace('@', '\@', $pattern);
109 return 1 === preg_match('@' . $pattern . '@', $haystack, $matches) ? $matches : null;
110 }
111
112 private function replace(string $url, string $protocol): string
113 {
114 $maybeProtocol = !$protocol ? 'https://' : '';
115 return sprintf(
116 '<a%s href="%s">%s</a>',
117 $this->additionalAttributes(),
118 $maybeProtocol . $url,
119 $url
120 );
121 }
122
123 protected function additionalAttributes(): string
124 {
125 if ($this->open_in_new_tab) {
126 return ' target="_blank" rel="noopener"';
127 }
128
129 return '';
130 }
131}
replaceMatches(string $from, callable $replace)
__construct($open_in_new_tab=true)
regexPos(string $regexp, string $string)
transform($from)
Perform the transformation.
shouldReplace(string $maybeHTML, int $startOfMatch, int $endOfMatch)
match(string $pattern, string $haystack)
replace(string $url, string $protocol)
A transformation is a function from one datatype to another.
$url
Definition: shib_logout.php:68