ILIAS  release_8 Revision v8.23
MakeClickable.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
21 namespace ILIAS\Refinery\String;
22 
27 use Closure;
28 
29 class MakeClickable implements Transformation
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 }
transform($from)
Perform the transformation.
replace(string $url, string $protocol)
match(string $pattern, string $haystack)
replaceMatches(string $from, callable $replace)
__construct($open_in_new_tab=true)
regexPos(string $regexp, string $string)
A transformation is a function from one datatype to another.
shouldReplace(string $maybeHTML, int $startOfMatch, int $endOfMatch)
$url
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Definition: CaseOfLabel.php:21
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...