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