From 9b3a25120e5ef0140f7c3c13ff7d36d66056604d Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Mon, 18 Aug 2025 00:46:16 -0600 Subject: [PATCH 1/6] Use ENT_QUOTES in existing calls to Utils::htmlspecialcharsRecursive() This is necessary because the htmlspecialchars__recursive() function in 2.x always used ENT_QUOTES, whereas Utils::htmlspecialcharsRecursive() defaults to ENT_COMPAT in order to align with Utils::htmlspecialchars(). Signed-off-by: Jon Stovell --- Sources/Actions/Admin/Themes.php | 2 +- Sources/Actions/Post.php | 2 +- Sources/Actions/Profile/Main.php | 2 +- Sources/Actions/Register2.php | 2 +- Sources/PackageManager/PackageUtils.php | 2 +- Sources/Poll.php | 2 +- Sources/Profile.php | 2 +- Sources/QueryString.php | 2 +- Sources/Subs-Compat.php | 2 +- Sources/Utils.php | 3 +++ 10 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Sources/Actions/Admin/Themes.php b/Sources/Actions/Admin/Themes.php index 4702427340..a59d0897af 100644 --- a/Sources/Actions/Admin/Themes.php +++ b/Sources/Actions/Admin/Themes.php @@ -801,7 +801,7 @@ public function setSettings(): void foreach (Theme::$current->settings as $setting => $dummy) { if (!in_array($setting, ['theme_url', 'theme_dir', 'images_url', 'template_dirs'])) { - Theme::$current->settings[$setting] = Utils::htmlspecialcharsRecursive(Theme::$current->settings[$setting]); + Theme::$current->settings[$setting] = Utils::htmlspecialcharsRecursive(Theme::$current->settings[$setting], ENT_QUOTES); } } diff --git a/Sources/Actions/Post.php b/Sources/Actions/Post.php index 363d97559d..5586d79972 100644 --- a/Sources/Actions/Post.php +++ b/Sources/Actions/Post.php @@ -970,7 +970,7 @@ protected function showPreview(): void Utils::$context['choices'] = []; $choice_id = 0; - $_POST['options'] = empty($_POST['options']) ? [] : Utils::htmlspecialcharsRecursive($_POST['options']); + $_POST['options'] = empty($_POST['options']) ? [] : Utils::htmlspecialcharsRecursive($_POST['options'], ENT_QUOTES); foreach ($_POST['options'] as $option) { if (trim($option) == '') { diff --git a/Sources/Actions/Profile/Main.php b/Sources/Actions/Profile/Main.php index b33a6142f4..e9c62a7769 100644 --- a/Sources/Actions/Profile/Main.php +++ b/Sources/Actions/Profile/Main.php @@ -642,7 +642,7 @@ public function execute(): void if (Utils::$context['completed_save']) { // Clean up the POST variables. $_POST = Utils::htmlTrimRecursive($_POST); - $_POST = Utils::htmlspecialcharsRecursive($_POST); + $_POST = Utils::htmlspecialcharsRecursive($_POST, ENT_QUOTES); Profile::$member->post_sanitized = true; if ($this->check_password) { diff --git a/Sources/Actions/Register2.php b/Sources/Actions/Register2.php index cf49ba52ef..21a3ab20b1 100644 --- a/Sources/Actions/Register2.php +++ b/Sources/Actions/Register2.php @@ -296,7 +296,7 @@ function (&$value, $key) { } // Make sure they are clean, dammit! - $reg_options['theme_vars'] = Utils::htmlspecialcharsRecursive($reg_options['theme_vars']); + $reg_options['theme_vars'] = Utils::htmlspecialcharsRecursive($reg_options['theme_vars'], ENT_QUOTES); // Check whether we have fields that simply MUST be displayed? $request = Db::$db->query( diff --git a/Sources/PackageManager/PackageUtils.php b/Sources/PackageManager/PackageUtils.php index d756f07a39..a6911e5287 100644 --- a/Sources/PackageManager/PackageUtils.php +++ b/Sources/PackageManager/PackageUtils.php @@ -506,7 +506,7 @@ public static function loadInstalledPackages(): array $found[] = $row['package_id']; - $row = Utils::htmlspecialcharsRecursive($row); + $row = Utils::htmlspecialcharsRecursive($row, ENT_QUOTES); $installed[] = [ 'id' => $row['id_install'], diff --git a/Sources/Poll.php b/Sources/Poll.php index ccd58afe6e..be7b548aeb 100644 --- a/Sources/Poll.php +++ b/Sources/Poll.php @@ -1009,7 +1009,7 @@ public static function sanitizeInput(array &$errors): void $_POST['question'] = Utils::htmlspecialchars($_POST['question']); $_POST['question'] = Utils::truncate($_POST['question'], 255); $_POST['question'] = preg_replace('~&#(\d{4,5}|[2-9]\d{2,4}|1[2-9]\d);~', '&#$1;', $_POST['question']); - $_POST['options'] = Utils::htmlspecialcharsRecursive($_POST['options']); + $_POST['options'] = Utils::htmlspecialcharsRecursive($_POST['options'], ENT_QUOTES); } /****************** diff --git a/Sources/Profile.php b/Sources/Profile.php index 93da2b9961..e53b9f323e 100644 --- a/Sources/Profile.php +++ b/Sources/Profile.php @@ -1519,7 +1519,7 @@ public function save(): void // If $_POST hasn't already been sanitized, do that now. if (!$this->post_sanitized) { $_POST = Utils::htmlTrimRecursive($_POST); - $_POST = Utils::htmlspecialcharsRecursive($_POST); + $_POST = Utils::htmlspecialcharsRecursive($_POST, ENT_QUOTES); $this->post_sanitized = true; } diff --git a/Sources/QueryString.php b/Sources/QueryString.php index e8c6fbdaf4..81abc99364 100644 --- a/Sources/QueryString.php +++ b/Sources/QueryString.php @@ -158,7 +158,7 @@ public static function cleanRequest(): void } // Add entities to GET. This is kinda like the slashes on everything else. - $_GET = Utils::htmlspecialcharsRecursive($_GET); + $_GET = Utils::htmlspecialcharsRecursive($_GET, ENT_QUOTES); // Let's not depend on the ini settings... why even have COOKIE in there, anyway? $_REQUEST = $_POST + $_GET; diff --git a/Sources/Subs-Compat.php b/Sources/Subs-Compat.php index e63c85bc8b..e61959bce1 100644 --- a/Sources/Subs-Compat.php +++ b/Sources/Subs-Compat.php @@ -11398,7 +11398,7 @@ function normalize_spaces($string, $vspace = true, $hspace = false, $options = [ */ function htmlspecialchars__recursive(array|string $var, int $level = 0): array|string { - return SMF\Utils::htmlspecialcharsRecursive($var); + return SMF\Utils::htmlspecialcharsRecursive($var, ENT_QUOTES); } /** diff --git a/Sources/Utils.php b/Sources/Utils.php index 8c0acc4125..5000b31585 100644 --- a/Sources/Utils.php +++ b/Sources/Utils.php @@ -559,6 +559,9 @@ public static function htmlspecialchars(string $string, int $flags = ENT_COMPAT, * * Only affects values. * + * Note that the default value of $flags is ENT_COMPAT, whereas SMF 2.x's + * htmlspecialchars__recursive() function always used ENT_QUOTES. + * * @param mixed $var The string or array of strings to add entities to * @param int $flags Bitmask of flags to pass to standard htmlspecialchars(). * Default is ENT_COMPAT. From f37b4392ca0fa83e09c79101b58dafab4937dfb0 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Mon, 18 Aug 2025 01:04:52 -0600 Subject: [PATCH 2/6] Adds $double_encode param to Utils::htmlspecialchars() and friends Signed-off-by: Jon Stovell --- Sources/Utils.php | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/Sources/Utils.php b/Sources/Utils.php index 5000b31585..28299448e3 100644 --- a/Sources/Utils.php +++ b/Sources/Utils.php @@ -542,16 +542,16 @@ public static function normalizeSpaces(string $string, bool $vspace = true, bool * sanitizes the string. * * @param string $string The string being converted. - * @param int $flags Bitmask of flags to pass to standard htmlspecialchars(). - * Default is ENT_COMPAT. - * @param string $encoding Character encoding. Default is UTF-8. + * @param int $flags Bitmask of flags to pass to \htmlspecialchars(). + * Default: ENT_COMPAT. + * @param string $encoding Character encoding. Default: 'UTF-8'. + * @param bool $double_encode Whether to encode apersands in existing + * entities (e.g. '"' --> '&quot;'). Default: true. * @return string The converted string. */ - public static function htmlspecialchars(string $string, int $flags = ENT_COMPAT, string $encoding = 'UTF-8'): string + public static function htmlspecialchars(string $string, int $flags = ENT_COMPAT, string $encoding = 'UTF-8', bool $double_encode = true): string { - $string = self::normalize($string); - - return self::sanitizeEntities(\htmlspecialchars($string, $flags, $encoding)); + return self::sanitizeEntities(\htmlspecialchars(self::normalize($string), $flags, $encoding, $double_encode)); } /** @@ -563,17 +563,19 @@ public static function htmlspecialchars(string $string, int $flags = ENT_COMPAT, * htmlspecialchars__recursive() function always used ENT_QUOTES. * * @param mixed $var The string or array of strings to add entities to - * @param int $flags Bitmask of flags to pass to standard htmlspecialchars(). - * Default is ENT_COMPAT. - * @param string $encoding Character encoding. Default is UTF-8. + * @param int $flags Bitmask of flags to pass to \htmlspecialchars(). + * Default: ENT_COMPAT. + * @param string $encoding Character encoding. Default: 'UTF-8'. + * @param bool $double_encode Whether to encode apersands in existing + * entities (e.g. '"' --> '&quot;'). Default: true. * @return array|string The string or array of strings with entities added */ - public static function htmlspecialcharsRecursive(mixed $var, int $flags = ENT_COMPAT, string $encoding = 'UTF-8'): array|string + public static function htmlspecialcharsRecursive(mixed $var, int $flags = ENT_COMPAT, string $encoding = 'UTF-8', bool $double_encode = true): array|string { static $level = 0; if (!is_array($var)) { - return self::htmlspecialchars((string) $var, $flags, $encoding); + return self::htmlspecialchars((string) $var, $flags, $encoding, $double_encode); } // Add the htmlspecialchars to every element. @@ -582,7 +584,7 @@ public static function htmlspecialcharsRecursive(mixed $var, int $flags = ENT_CO $var[$k] = null; } else { $level++; - $var[$k] = self::htmlspecialcharsRecursive($v, $flags, $encoding); + $var[$k] = self::htmlspecialcharsRecursive($v, $flags, $encoding, $double_encode); $level--; } } From 14df9ad90c8bda6cff8fee7250847aad51f81d40 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Mon, 18 Aug 2025 00:56:12 -0600 Subject: [PATCH 3/6] Adds SMF\Utils::htmlspecialcharsDecodeRecursive() Signed-off-by: Jon Stovell --- Sources/Utils.php | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/Sources/Utils.php b/Sources/Utils.php index 28299448e3..3696293aa0 100644 --- a/Sources/Utils.php +++ b/Sources/Utils.php @@ -599,9 +599,9 @@ public static function htmlspecialcharsRecursive(mixed $var, int $flags = ENT_CO * replaces ' ' with a simple space character. * * @param string $string A string. - * @param int $flags Bitmask of flags to pass to standard htmlspecialchars(). - * Default is ENT_QUOTES. - * @param string $encoding Character encoding. Default is UTF-8. + * @param int $flags Bitmask of flags to pass to \htmlspecialchars(). + * Default: ENT_QUOTES. + * @param string $encoding Character encoding. Default: 'UTF-8'. * @return string|false The string without entities, or false on failure. */ public static function htmlspecialcharsDecode(string $string, int $flags = ENT_QUOTES, string $encoding = 'UTF-8'): string|false @@ -609,6 +609,40 @@ public static function htmlspecialcharsDecode(string $string, int $flags = ENT_Q return preg_replace('/' . self::ENT_NBSP . '/u', ' ', htmlspecialchars_decode($string, $flags)); } + /** + * Recursively applies self::htmlspecialcharsDecode() to all elements of an + * array. + * + * Only affects values. + * + * @param mixed $var The string or array of strings to add entities to + * @param int $flags Bitmask of flags to pass to \htmlspecialchars(). + * Default: ENT_COMPAT. + * @param string $encoding Character encoding. Default: 'UTF-8'. + * @return array|string The string or array of strings without entities. + */ + public static function htmlspecialcharsDecodeRecursive(mixed $var, int $flags = ENT_COMPAT, string $encoding = 'UTF-8'): array|string + { + static $level = 0; + + if (!is_array($var)) { + return self::htmlspecialcharsDecode((string) $var, $flags, $encoding); + } + + // Decode the htmlspecialchars in every element. + foreach ($var as $k => $v) { + if ($level > 25) { + $var[$k] = null; + } else { + $level++; + $var[$k] = self::htmlspecialcharsDecodeRecursive($v, $flags, $encoding); + $level--; + } + } + + return $var; + } + /** * Like standard ltrim(), except that it also trims   entities, control * characters, and Unicode whitespace characters beyond the ASCII range. From 78c3cc09df9d42ec28b4adb147801a5b8aeb8ccc Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Mon, 18 Aug 2025 15:46:42 -0600 Subject: [PATCH 4/6] Simplifies entity processing methods Signed-off-by: Jon Stovell --- Sources/Subs-Compat.php | 144 ++++++++++++++++++++++++++++++++++++++++ Sources/Utils.php | 64 +++++------------- 2 files changed, 161 insertions(+), 47 deletions(-) diff --git a/Sources/Subs-Compat.php b/Sources/Subs-Compat.php index e61959bce1..7995831582 100644 --- a/Sources/Subs-Compat.php +++ b/Sources/Subs-Compat.php @@ -12090,3 +12090,147 @@ function array_find_key(array $array, callable $callback): mixed return null; } } + +if (!function_exists('grapheme_str_split')) { + function grapheme_str_split(string $string, int $length = 1): array|false + { + if ($length < 1 || $length > 1073741823) { + throw new \ValueError('grapheme_str_split(): Argument #2 ($length) must be greater than 0 and less than or equal to 1073741823'); + } + + try { + return preg_split('/(\X{' . $length . '})/u', $string, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + } catch (\Throwable $e) { + return false; + } + } +} + +if (!function_exists('grapheme_strlen')) { + function grapheme_strlen(string $string): int|false|null + { + if ( + @preg_match('//u', $string) === false + && preg_last_error() === PREG_BAD_UTF8_ERROR + ) { + return null; + } + + return count(grapheme_str_split($string, 1)); + } +} + +if (!function_exists('grapheme_substr')) { + function grapheme_substr(string $string, int $offset = 0, ?int $length = null): string|false + { + if (($graphemes = grapheme_str_split($string, 1)) === false) { + return false; + } + + return implode('', array_slice($graphemes, $offset, $length)); + } +} + +if (!function_exists('grapheme_strpos')) { + function grapheme_strpos(string $haystack, string $needle, int $offset = 0): int|false + { + if (!str_contains($haystack, $needle)) { + return false; + } + + $haystack = grapheme_str_split($haystack, 1); + + if ($haystack === false) { + return false; + } + + if (abs($offset) >= count($haystack)) { + throw new \ValueError('grapheme_strpos(): Argument #3 ($offset) must be contained in argument #1 ($haystack)'); + } + + $skipped = array_splice($haystack, 0, $offset); + + $haystack = implode('', $haystack); + + $before = grapheme_str_split(substr($haystack, 0, strpos($haystack, $needle)), 1); + + return $before === false ? false : count($skipped) + count($before); + } +} + +if (!function_exists('grapheme_stripos')) { + function grapheme_stripos(string $haystack, string $needle, int $offset = 0): int|false + { + $haystack = mb_convert_case($haystack, MB_CASE_FOLD_SIMPLE); + $needle = mb_convert_case($needle, MB_CASE_FOLD_SIMPLE); + + return grapheme_strpos($haystack, $needle, $offset); + } +} + +if (!function_exists('grapheme_strrpos')) { + function grapheme_strrpos(string $haystack, string $needle, int $offset = 0): int|false + { + if (!str_contains($haystack, $needle)) { + return false; + } + + $haystack = grapheme_str_split($haystack, 1); + $needle = grapheme_str_split($needle, 1); + + if ($haystack === false || $needle === false) { + return false; + } + + $haystack_len = count($haystack); + $needle_len = count($needle); + + if (abs($offset) >= $haystack_len) { + throw new \ValueError('grapheme_strrpos(): Argument #3 ($offset) must be contained in argument #1 ($haystack)'); + } + + if ($offset < 0) { + $offset = ($haystack_len + $offset) % $haystack_len; + + for ($i = $offset; $i > -1; $i--) { + if (array_slice($haystack, $i, $needle_len) === $needle) { + break; + } + } + + return $i < 0 ? false : $i; + } + + for ($i = $haystack_len; $i > $offset; $i--) { + if (array_slice($haystack, $i, $needle_len) === $needle) { + break; + } + } + + return $i >= $haystack_len - $needle_len ? false : $i; + } +} + +if (!function_exists('grapheme_strripos')) { + function grapheme_strripos(string $haystack, string $needle, int $offset = 0): int|false + { + $haystack = mb_convert_case($haystack, MB_CASE_FOLD_SIMPLE); + $needle = mb_convert_case($needle, MB_CASE_FOLD_SIMPLE); + + return grapheme_strrpos($haystack, $needle, $offset); + } +} + +if (!function_exists('grapheme_strstr')) { + function grapheme_strstr(string $haystack, string $needle, bool $before_needle = false): string|false + { + return $before_needle ? grapheme_substr($haystack, 0, grapheme_strpos($haystack, $needle)) : grapheme_substr($haystack, grapheme_strpos($haystack, $needle)); + } +} + +if (!function_exists('grapheme_stristr')) { + function grapheme_strstr(string $haystack, string $needle, bool $before_needle = false): string|false + { + return $before_needle ? grapheme_substr($haystack, 0, grapheme_stripos($haystack, $needle)) : grapheme_substr($haystack, grapheme_stripos($haystack, $needle)); + } +} diff --git a/Sources/Utils.php b/Sources/Utils.php index 3696293aa0..7d0e95e6a3 100644 --- a/Sources/Utils.php +++ b/Sources/Utils.php @@ -362,7 +362,7 @@ public static function entityDecode(string $string, int $flags = ENT_QUOTES | EN return $string; } - // Enables consistency with the behaviour of un_htmlspecialchars. + // Enables consistency with the behaviour of self::htmlspecialcharsDecode(). if ($nbsp_to_space) { $string = preg_replace('~' . self::ENT_NBSP . '~u', ' ', $string); } @@ -388,7 +388,7 @@ public static function entityFix(string $string): string } /** - * Replaces HTML entities for invalid characters with a substitute. + * Replaces numeric entities for invalid characters with a substitute. * * The default substitute is the entity for the replacement character U+FFFD * (a.k.a. the question-mark-in-a-box). @@ -711,7 +711,7 @@ public static function htmlTrimRecursive(array|string $var): array|string|false } /** - * Like standard mb_strlen(), except that it counts HTML entities as + * Like standard grapheme_strlen(), except that it counts HTML entities as * single characters. This essentially amounts to getting the length of * the string as it would appear to a human reader. * @@ -720,11 +720,11 @@ public static function htmlTrimRecursive(array|string $var): array|string|false */ public static function entityStrlen(string $string): int { - return strlen((string) preg_replace('~' . self::ENT_LIST . '|\X~u', '_', self::sanitizeEntities($string))); + return grapheme_strlen(self::entityDecode($string)); } /** - * Like standard mb_strpos(), except that it counts HTML entities as + * Like standard grapheme_strpos(), except that it counts HTML entities as * single characters. * * @param string $haystack The string to search in. @@ -734,35 +734,11 @@ public static function entityStrlen(string $string): int */ public static function entityStrpos(string $haystack, string $needle, int $offset = 0): int|false { - $haystack_arr = (array) preg_split('~(' . self::ENT_LIST . '|\X)~u', self::sanitizeEntities($haystack), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); - - if (strlen($needle) === 1) { - $result = array_search($needle, array_slice($haystack_arr, $offset)); - - return is_int($result) ? $result + $offset : false; - } - - $needle_arr = (array) preg_split('~(' . self::ENT_LIST . '|\X)~u', self::sanitizeEntities($needle), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); - - $needle_size = count($needle_arr); - - $result = array_search($needle_arr[0], array_slice($haystack_arr, $offset)); - - while ((int) $result === $result) { - $offset += $result; - - if (array_slice($haystack_arr, $offset, $needle_size) === $needle_arr) { - return $offset; - } - - $result = array_search($needle_arr[0], array_slice($haystack_arr, ++$offset)); - } - - return false; + return grapheme_strpos(self::entityDecode($haystack), self::entityDecode($needle), $offset); } /** - * Like standard mb_substr(), except that it counts HTML entities as + * Like standard grapheme_substr(), except that it counts HTML entities as * single characters. * * @param string $string The input string. @@ -772,14 +748,16 @@ public static function entityStrpos(string $haystack, string $needle, int $offse */ public static function entitySubstr(string $string, int $offset, ?int $length = null): string { - $ent_arr = (array) preg_split('~(' . self::ENT_LIST . '|\X)~u', self::sanitizeEntities($string), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + if (!str_contains($string, '&')) { + return (string) grapheme_substr($string, $offset, $length); + } - return $length === null ? implode('', array_slice($ent_arr, $offset)) : implode('', array_slice($ent_arr, $offset, $length)); + return implode('', array_slice(self::entityStrSplit($string), $offset, $length)); } /** - * Like standard mb_str_split(), except that it counts HTML entities as - * single characters. + * Like standard grapheme_str_split(), except that it counts HTML entities + * as single characters. * * @param string $string The input string. * @param int $length Maximum character length of the substrings to return. @@ -788,22 +766,14 @@ public static function entitySubstr(string $string, int $offset, ?int $length = public static function entityStrSplit(string $string, int $length = 1): array { if ($length < 1) { - throw new \ValueError(); + throw new \ValueError(__METHOD__ . ': Argument #2 ($length) must be greater than 0'); } - $ent_arr = (array) preg_split('~(' . Utils::ENT_LIST . '|\X)~u', Utils::sanitizeEntities($string), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); - - if ($length > 1) { - $temp = []; - - while (!empty($ent_arr)) { - $temp[] = implode('', array_splice($ent_arr, 0, $length)); - } - - $ent_arr = $temp; + if (!str_contains($string, '&')) { + return (array) grapheme_str_split($string, $length); } - return $ent_arr; + return (array) preg_split('~((?:' . self::ENT_LIST . '|\X){' . $length . '})~u', self::sanitizeEntities($string), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); } /** From f264aa3ec9c0729955a7f5ff3c4df4578c83c374 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Tue, 19 Aug 2025 11:48:21 -0600 Subject: [PATCH 5/6] Updates jquery.atwho.min.js to version 1.5.4 Signed-off-by: Jon Stovell --- Themes/default/scripts/jquery.atwho.min.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Themes/default/scripts/jquery.atwho.min.js b/Themes/default/scripts/jquery.atwho.min.js index e5d9dfada3..857bb93126 100644 --- a/Themes/default/scripts/jquery.atwho.min.js +++ b/Themes/default/scripts/jquery.atwho.min.js @@ -1 +1 @@ -!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof exports?module.exports=b(require("jquery")):b(jQuery)}(this,function(a){var b,c,d,e,f,g,h,i,j,k=[].slice,l=function(a,b){function c(){this.constructor=a}for(var d in b)m.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},m={}.hasOwnProperty;c=function(){function a(a){this.currentFlag=null,this.controllers={},this.aliasMaps={},this.$inputor=$(a),this.setupRootElement(),this.listen()}return a.prototype.createContainer=function(a){var b;return null!=(b=this.$el)&&b.remove(),$(a.body).append(this.$el=$("
"))},a.prototype.setupRootElement=function(a,b){var c;if(null==b&&(b=!1),a)this.window=a.contentWindow,this.document=a.contentDocument||this.window.document,this.iframe=a;else{this.document=this.$inputor[0].ownerDocument,this.window=this.document.defaultView||this.document.parentWindow;try{this.iframe=this.window.frameElement}catch(d){if(c=d,this.iframe=null,$.fn.atwho.debug)throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n"+c)}}return this.createContainer((this.iframeAsRoot=b)?this.document:document)},a.prototype.controller=function(a){var b,c,d,e;if(this.aliasMaps[a])c=this.controllers[this.aliasMaps[a]];else{e=this.controllers;for(d in e)if(b=e[d],d===a){c=b;break}}return c?c:this.controllers[this.currentFlag]},a.prototype.setContextFor=function(a){return this.currentFlag=a,this},a.prototype.reg=function(a,b){var c,d;return d=(c=this.controllers)[a]||(c[a]=this.$inputor.is("[contentEditable]")?new f(this,a):new i(this,a)),b.alias&&(this.aliasMaps[b.alias]=a),d.init(b),this},a.prototype.listen=function(){return this.$inputor.on("compositionstart",function(a){return function(b){var c;return null!=(c=a.controller())&&c.view.hide(),a.isComposing=!0}}(this)).on("compositionend",function(a){return function(b){return a.isComposing=!1}}(this)).on("keyup.atwhoInner",function(a){return function(b){return a.onKeyup(b)}}(this)).on("keydown.atwhoInner",function(a){return function(b){return a.onKeydown(b)}}(this)).on("scroll.atwhoInner",function(a){return function(b){var c;return null!=(c=a.controller())?c.view.hide(b):void 0}}(this)).on("blur.atwhoInner",function(a){return function(b){var c;return(c=a.controller())?c.view.hide(b,c.getOpt("displayTimeout")):void 0}}(this)).on("click.atwhoInner",function(a){return function(b){return a.dispatch(b)}}(this))},a.prototype.shutdown=function(){var a,b,c;c=this.controllers;for(a in c)b=c[a],b.destroy(),delete this.controllers[a];return this.$inputor.off(".atwhoInner"),this.$el.remove()},a.prototype.dispatch=function(a){var b,c,d,e;d=this.controllers,e=[];for(b in d)c=d[b],e.push(c.lookUp(a));return e},a.prototype.onKeyup=function(a){var b;switch(a.keyCode){case g.ESC:a.preventDefault(),null!=(b=this.controller())&&b.view.hide();break;case g.DOWN:case g.UP:case g.CTRL:$.noop();break;case g.P:case g.N:a.ctrlKey||this.dispatch(a);break;default:this.dispatch(a)}},a.prototype.onKeydown=function(a){var b,c;if(c=null!=(b=this.controller())?b.view:void 0,c&&c.visible())switch(a.keyCode){case g.ESC:a.preventDefault(),c.hide(a);break;case g.UP:a.preventDefault(),c.prev();break;case g.DOWN:a.preventDefault(),c.next();break;case g.P:if(!a.ctrlKey)return;a.preventDefault(),c.prev();break;case g.N:if(!a.ctrlKey)return;a.preventDefault(),c.next();break;case g.TAB:case g.ENTER:case g.SPACE:if(!c.visible())return;if(!this.controller().getOpt("spaceSelectsMatch")&&a.keyCode===g.SPACE)return;c.highlighted()?(a.preventDefault(),c.choose(a)):c.hide(a);break;default:$.noop()}},a}(),d=function(){function a(a,b){this.app=a,this.at=b,this.$inputor=this.app.$inputor,this.id=this.$inputor[0].id||this.uid(),this.setting=null,this.query=null,this.pos=0,this.range=null,0===(this.$el=$("#atwho-ground-"+this.id,this.app.$el)).length&&this.app.$el.append(this.$el=$("
")),this.model=new h(this),this.view=new j(this)}return a.prototype.uid=function(){return(Math.random().toString(16)+"000000000").substr(2,8)+(new Date).getTime()},a.prototype.init=function(a){return this.setting=$.extend({},this.setting||$.fn.atwho["default"],a),this.view.init(),this.model.reload(this.setting.data)},a.prototype.destroy=function(){return this.trigger("beforeDestroy"),this.model.destroy(),this.view.destroy(),this.$el.remove()},a.prototype.callDefault=function(){var a,b,c;c=arguments[0],a=2<=arguments.length?k.call(arguments,1):[];try{return e[c].apply(this,a)}catch(d){return b=d,$.error(b+" Or maybe At.js doesn't have function "+c)}},a.prototype.trigger=function(a,b){var c,d;return null==b&&(b=[]),b.push(this),c=this.getOpt("alias"),d=c?a+"-"+c+".atwho":a+".atwho",this.$inputor.trigger(d,b)},a.prototype.callbacks=function(a){return this.getOpt("callbacks")[a]||e[a]},a.prototype.getOpt=function(a,b){var c;try{return this.setting[a]}catch(d){return c=d,null}},a.prototype.insertContentFor=function(a){var b,c;return c=this.getOpt("insertTpl"),b=$.extend({},a.data("item-data"),{"atwho-at":this.at}),this.callbacks("tplEval").call(this,c,b,"onInsert")},a.prototype.renderView=function(a){var b;return b=this.getOpt("searchKey"),a=this.callbacks("sorter").call(this,this.query.text,a.slice(0,1001),b),this.view.render(a.slice(0,this.getOpt("limit")))},a.arrayToDefaultHash=function(a){var b,c,d,e;if(!$.isArray(a))return a;for(e=[],b=0,d=a.length;d>b;b++)c=a[b],e.push($.isPlainObject(c)?c:{name:c});return e},a.prototype.lookUp=function(a){var b,c;if(b=this.catchQuery(a))return this.app.setContextFor(this.at),(c=this.getOpt("delay"))?this._delayLookUp(b,c):this._lookUp(b),b},a.prototype._delayLookUp=function(a,b){var c,d;return c=Date.now?Date.now():(new Date).getTime(),this.previousCallTime||(this.previousCallTime=c),d=b-(c-this.previousCallTime),d>0&&b>d?(this.previousCallTime=c,this._stopDelayedCall(),this.delayedCallTimeout=setTimeout(function(b){return function(){return b.previousCallTime=0,b.delayedCallTimeout=null,b._lookUp(a)}}(this),b)):(this._stopDelayedCall(),this.previousCallTime!==c&&(this.previousCallTime=0),this._lookUp(a))},a.prototype._stopDelayedCall=function(){return this.delayedCallTimeout?(clearTimeout(this.delayedCallTimeout),this.delayedCallTimeout=null):void 0},a.prototype._lookUp=function(a){var b;return b=function(a){return a&&a.length>0?this.renderView(this.constructor.arrayToDefaultHash(a)):this.view.hide()},this.model.query(a.text,$.proxy(b,this))},a}(),i=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return l(b,a),b.prototype.catchQuery=function(){var a,b,c,d,e,f;return b=this.$inputor.val(),a=this.$inputor.caret("pos",{iframe:this.app.iframe}),f=b.slice(0,a),d=this.callbacks("matcher").call(this,this.at,f,this.getOpt("startWithSpace")),"string"==typeof d&&d.length<=this.getOpt("maxLen",20)?(e=a-d.length,c=e+d.length,this.pos=e,d={text:d,headPos:e,endPos:c},this.trigger("matched",[this.at,d.text])):(d=null,this.view.hide()),this.query=d},b.prototype.rect=function(){var a,b,c;if(a=this.$inputor.caret("offset",this.pos-1,{iframe:this.app.iframe}))return this.app.iframe&&!this.app.iframeAsRoot&&(b=$(this.app.iframe).offset(),a.left+=b.left,a.top+=b.top),c=this.app.document.selection?0:2,{left:a.left,top:a.top,bottom:a.top+a.height+c}},b.prototype.insert=function(a,b){var c,d,e,f,g;return c=this.$inputor,d=c.val(),e=d.slice(0,Math.max(this.query.headPos-this.at.length,0)),f=""===(f=this.getOpt("suffix"))?f:f||" ",a+=f,g=""+e+a+d.slice(this.query.endPos||0),c.val(g),c.caret("pos",e.length+a.length,{iframe:this.app.iframe}),c.is(":focus")||c.focus(),c.change()},b}(d),f=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return l(b,a),b.prototype._getRange=function(){var a;return a=this.app.window.getSelection(),a.rangeCount>0?a.getRangeAt(0):void 0},b.prototype._setRange=function(a,b,c){return null==c&&(c=this._getRange()),c?(b=$(b)[0],"after"===a?(c.setEndAfter(b),c.setStartAfter(b)):(c.setEndBefore(b),c.setStartBefore(b)),c.collapse(!1),this._clearRange(c)):void 0},b.prototype._clearRange=function(a){var b;return null==a&&(a=this._getRange()),b=this.app.window.getSelection(),null==this.ctrl_a_pressed?(b.removeAllRanges(),b.addRange(a)):void 0},b.prototype._movingEvent=function(a){var b;return"click"===a.type||(b=a.which)===g.RIGHT||b===g.LEFT||b===g.UP||b===g.DOWN},b.prototype._unwrap=function(a){var b;return a=$(a).unwrap().get(0),(b=a.nextSibling)&&b.nodeValue&&(a.nodeValue+=b.nodeValue,$(b).remove()),a},b.prototype.catchQuery=function(a){var b,c,d,e,f,h,i,j,k,l;if(!this.app.isComposing&&(l=this._getRange())){if(a.which===g.CTRL?this.ctrl_pressed=!0:a.which===g.A?null==this.ctrl_pressed&&(this.ctrl_a_pressed=!0):(delete this.ctrl_a_pressed,delete this.ctrl_pressed),a.which===g.ENTER)return(c=$(l.startContainer).closest(".atwho-query")).contents().unwrap(),c.is(":empty")&&c.remove(),(c=$(".atwho-query",this.app.document)).text(c.text()).contents().last().unwrap(),void this._clearRange();if(/firefox/i.test(navigator.userAgent)){if($(l.startContainer).is(this.$inputor))return void this._clearRange();a.which===g.BACKSPACE&&l.startContainer.nodeType===document.ELEMENT_NODE&&(j=l.startOffset-1)>=0?(d=l.cloneRange(),d.setStart(l.startContainer,j),$(d.cloneContents()).contents().last().is(".atwho-inserted")&&(f=$(l.startContainer).contents().get(j),this._setRange("after",$(f).contents().last()))):a.which===g.LEFT&&l.startContainer.nodeType===document.TEXT_NODE&&(b=$(l.startContainer.previousSibling),b.is(".atwho-inserted")&&0===l.startOffset&&this._setRange("after",b.contents().last()))}return $(l.startContainer).closest(".atwho-inserted").addClass("atwho-query").siblings().removeClass("atwho-query"),(c=$(".atwho-query",this.app.document)).length>0&&c.is(":empty")&&0===c.text().length&&c.remove(),this._movingEvent(a)||c.removeClass("atwho-inserted"),d=l.cloneRange(),d.setStart(l.startContainer,0),i=this.callbacks("matcher").call(this,this.at,d.toString(),this.getOpt("startWithSpace")),0===c.length&&"string"==typeof i&&(e=l.startOffset-this.at.length-i.length)>=0&&(l.setStart(l.startContainer,e),c=$("",this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass("atwho-query"),l.surroundContents(c.get(0)),h=c.contents().last().get(0),/firefox/i.test(navigator.userAgent)?(l.setStart(h,h.length),l.setEnd(h,h.length),this._clearRange(l)):this._setRange("after",h,l)),"string"==typeof i&&i.length<=this.getOpt("maxLen",20)?(k={text:i,el:c},this.trigger("matched",[this.at,k.text]),this.query=k):(this.view.hide(),this.query={el:c},c.text().indexOf(this.at)>=0&&(this._movingEvent(a)&&c.hasClass("atwho-inserted")?c.removeClass("atwho-query"):!1!==this.callbacks("afterMatchFailed").call(this,this.at,c)&&this._setRange("after",this._unwrap(c.text(c.text()).contents().first()))),null)}},b.prototype.rect=function(){var a,b,c;return c=this.query.el.offset(),this.app.iframe&&!this.app.iframeAsRoot&&(b=(a=$(this.app.iframe)).offset(),c.left+=b.left-this.$inputor.scrollLeft(),c.top+=b.top-this.$inputor.scrollTop()),c.bottom=c.top+this.query.el.height(),c},b.prototype.insert=function(a,b){var c,d,e;return d=(d=this.getOpt("suffix"))?d:d||" ",this.query.el.removeClass("atwho-query").addClass("atwho-inserted").html(a),(c=this._getRange())&&(c.setEndAfter(this.query.el[0]),c.collapse(!1),c.insertNode(e=this.app.document.createTextNode(d)),this._setRange("after",e,c)),this.$inputor.is(":focus")||this.$inputor.focus(),this.$inputor.change()},b}(d),h=function(){function a(a){this.context=a,this.at=this.context.at,this.storage=this.context.$inputor}return a.prototype.destroy=function(){return this.storage.data(this.at,null)},a.prototype.saved=function(){return this.fetch()>0},a.prototype.query=function(a,b){var c,d,e;return d=this.fetch(),e=this.context.getOpt("searchKey"),d=this.context.callbacks("filter").call(this.context,a,d,e)||[],c=this.context.callbacks("remoteFilter"),d.length>0||!c&&0===d.length?b(d):c.call(this.context,a,b)},a.prototype.fetch=function(){return this.storage.data(this.at)||[]},a.prototype.save=function(a){return this.storage.data(this.at,this.context.callbacks("beforeSave").call(this.context,a||[]))},a.prototype.load=function(a){return!this.saved()&&a?this._load(a):void 0},a.prototype.reload=function(a){return this._load(a)},a.prototype._load=function(a){return"string"==typeof a?$.ajax(a,{dataType:"json"}).done(function(a){return function(b){return a.save(b)}}(this)):this.save(a)},a}(),j=function(){function a(a){this.context=a,this.$el=$("
    "),this.timeoutID=null,this.context.$el.append(this.$el),this.bindEvent()}return a.prototype.init=function(){var a;return a=this.context.getOpt("alias")||this.context.at.charCodeAt(0),this.$el.attr({id:"at-view-"+a})},a.prototype.destroy=function(){return this.$el.remove()},a.prototype.bindEvent=function(){var a;return a=this.$el.find("ul"),a.on("mouseenter.atwho-view","li",function(b){return a.find(".cur").removeClass("cur"),$(b.currentTarget).addClass("cur")}).on("click.atwho-view","li",function(b){return function(c){return a.find(".cur").removeClass("cur"),$(c.currentTarget).addClass("cur"),b.choose(c),c.preventDefault()}}(this))},a.prototype.visible=function(){return this.$el.is(":visible")},a.prototype.highlighted=function(){return this.$el.find(".cur").length>0},a.prototype.choose=function(a){var b,c;return(b=this.$el.find(".cur")).length&&(c=this.context.insertContentFor(b),this.context.insert(this.context.callbacks("beforeInsert").call(this.context,c,b),b),this.context.trigger("inserted",[b,a]),this.hide(a)),this.context.getOpt("hideWithoutSuffix")?this.stopShowing=!0:void 0},a.prototype.reposition=function(a){var b,c,d,e;return b=this.context.app.iframeAsRoot?this.context.app.window:window,a.bottom+this.$el.height()-$(b).scrollTop()>$(b).height()&&(a.bottom=a.top-this.$el.height()),a.left>(d=$(b).width()-this.$el.width()-5)&&(a.left=d),c={left:a.left,top:a.bottom},null!=(e=this.context.callbacks("beforeReposition"))&&e.call(this.context,c),this.$el.offset(c),this.context.trigger("reposition",[c])},a.prototype.next=function(){var a,b;return a=this.$el.find(".cur").removeClass("cur"),b=a.next(),b.length||(b=this.$el.find("li:first")),b.addClass("cur"),this.scrollTop(Math.max(0,a.innerHeight()*(b.index()+2)-this.$el.height()))},a.prototype.prev=function(){var a,b;return a=this.$el.find(".cur").removeClass("cur"),b=a.prev(),b.length||(b=this.$el.find("li:last")),b.addClass("cur"),this.scrollTop(Math.max(0,a.innerHeight()*(b.index()+2)-this.$el.height()))},a.prototype.scrollTop=function(a){var b;return b=this.context.getOpt("scrollDuration"),b?this.$el.animate({scrollTop:a},b):this.$el.scrollTop(a)},a.prototype.show=function(){var a;return this.stopShowing?void(this.stopShowing=!1):(this.visible()||(this.$el.show(),this.$el.scrollTop(0),this.context.trigger("shown")),(a=this.context.rect())?this.reposition(a):void 0)},a.prototype.hide=function(a,b){var c;if(this.visible())return isNaN(b)?(this.$el.hide(),this.context.trigger("hidden",[a])):(c=function(a){return function(){return a.hide()}}(this),clearTimeout(this.timeoutID),this.timeoutID=setTimeout(c,b))},a.prototype.render=function(a){var b,c,d,e,f,g,h;if(!($.isArray(a)&&a.length>0))return void this.hide();for(this.$el.find("ul").empty(),c=this.$el.find("ul"),h=this.context.getOpt("displayTpl"),d=0,f=a.length;f>d;d++)e=a[d],e=$.extend({},e,{"atwho-at":this.context.at}),g=this.context.callbacks("tplEval").call(this.context,h,e,"onDisplay"),b=$(this.context.callbacks("highlighter").call(this.context,g,this.context.query.text)),b.data("item-data",e),c.append(b);return this.show(),this.context.getOpt("highlightFirst")?c.find("li:first").addClass("cur"):void 0},a}(),g={DOWN:40,UP:38,ESC:27,TAB:9,ENTER:13,CTRL:17,A:65,P:80,N:78,LEFT:37,UP:38,RIGHT:39,DOWN:40,BACKSPACE:8,SPACE:32},e={beforeSave:function(a){return d.arrayToDefaultHash(a)},matcher:function(a,b,c,d){var e,f,g,h,i;return a=a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&"),c&&(a="(?:^|\\s)"+a),e=decodeURI("%C3%80"),f=decodeURI("%C3%BF"),i=d?" ":"",h=new RegExp(a+"([A-Za-z"+e+"-"+f+"0-9_"+i+".+-]*)$|"+a+"([^\\x00-\\xff]*)$","gi"),g=h.exec(b),g?g[2]||g[1]:null},filter:function(a,b,c){var d,e,f,g;for(d=[],e=0,g=b.length;g>e;e++)f=b[e],~new String(f[c]).toLowerCase().indexOf(a.toLowerCase())&&d.push(f);return d},remoteFilter:null,sorter:function(a,b,c){var d,e,f,g;if(!a)return b;for(d=[],e=0,g=b.length;g>e;e++)f=b[e],f.atwho_order=new String(f[c]).toLowerCase().indexOf(a.toLowerCase()),f.atwho_order>-1&&d.push(f);return d.sort(function(a,b){return a.atwho_order-b.atwho_order})},tplEval:function(a,b){var c,d;d=a;try{return"string"!=typeof a&&(d=a(b)),d.replace(/\$\{([^\}]*)\}/g,function(a,c,d){return b[c]})}catch(e){return c=e,""}},highlighter:function(a,b){var c;return b?(c=new RegExp(">\\s*(\\w*?)("+b.replace("+","\\+")+")(\\w*)\\s*<","ig"),a.replace(c,function(a,b,c,d){return"> "+b+""+c+""+d+" <"})):a},beforeInsert:function(a,b){return a},beforeReposition:function(a){return a},afterMatchFailed:function(a,b){}},b={load:function(a,b){var c;return(c=this.controller(a))?c.model.load(b):void 0},isSelecting:function(){var a;return null!=(a=this.controller())?a.view.visible():void 0},hide:function(){var a;return null!=(a=this.controller())?a.view.hide():void 0},reposition:function(){var a;return(a=this.controller())?(a.view.reposition(a.rect()),console.log("reposition",a)):void 0},setIframe:function(a,b){return this.setupRootElement(a,b),null},run:function(){return this.dispatch()},destroy:function(){return this.shutdown(),this.$inputor.data("atwho",null)}},$.fn.atwho=function(a){var d,e;return d=arguments,e=null,this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function(){var f,g;return(g=(f=$(this)).data("atwho"))||f.data("atwho",g=new c(this)),"object"!=typeof a&&a?b[a]&&g?e=b[a].apply(g,Array.prototype.slice.call(d,1)):$.error("Method "+a+" does not exist on jQuery.atwho"):g.reg(a.at,a)}),e||this},$.fn.atwho["default"]={at:void 0,alias:void 0,data:null,displayTpl:"
  • ${name}
  • ",insertTpl:"${atwho-at}${name}",callbacks:e,searchKey:"name",suffix:void 0,hideWithoutSuffix:!1,startWithSpace:!0,highlightFirst:!0,limit:5,maxLen:20,displayTimeout:300,delay:null,spaceSelectsMatch:!1,editableAtwhoQueryAttrs:{},scrollDuration:150},$.fn.atwho.debug=!1}); \ No newline at end of file +!function(t,e){"function"==typeof define&&define.amd?define(["jquery"],function(t){return e(t)}):"object"==typeof exports?module.exports=e(require("jquery")):e(jQuery)}(this,function(t){var e,i;i={ESC:27,TAB:9,ENTER:13,CTRL:17,A:65,P:80,N:78,LEFT:37,UP:38,RIGHT:39,DOWN:40,BACKSPACE:8,SPACE:32},e={beforeSave:function(t){return r.arrayToDefaultHash(t)},matcher:function(t,e,i,n){var r,o,s,a,h;return t=t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&"),i&&(t="(?:^|\\s)"+t),r=decodeURI("%C3%80"),o=decodeURI("%C3%BF"),h=n?" ":"",a=new RegExp(t+"([A-Za-z"+r+"-"+o+"0-9_"+h+"'.+-]*)$|"+t+"([^\\x00-\\xff]*)$","gi"),s=a.exec(e),s?s[2]||s[1]:null},filter:function(t,e,i){var n,r,o,s;for(n=[],r=0,s=e.length;s>r;r++)o=e[r],~new String(o[i]).toLowerCase().indexOf(t.toLowerCase())&&n.push(o);return n},remoteFilter:null,sorter:function(t,e,i){var n,r,o,s;if(!t)return e;for(n=[],r=0,s=e.length;s>r;r++)o=e[r],o.atwho_order=new String(o[i]).toLowerCase().indexOf(t.toLowerCase()),o.atwho_order>-1&&n.push(o);return n.sort(function(t,e){return t.atwho_order-e.atwho_order})},tplEval:function(t,e){var i,n,r;r=t;try{return"string"!=typeof t&&(r=t(e)),r.replace(/\$\{([^\}]*)\}/g,function(t,i,n){return e[i]})}catch(n){return i=n,""}},highlighter:function(t,e){var i;return e?(i=new RegExp(">\\s*([^<]*?)("+e.replace("+","\\+")+")([^<]*)\\s*<","ig"),t.replace(i,function(t,e,i,n){return"> "+e+""+i+""+n+" <"})):t},beforeInsert:function(t,e,i){return t},beforeReposition:function(t){return t},afterMatchFailed:function(t,e){}};var n;n=function(){function e(e){this.currentFlag=null,this.controllers={},this.aliasMaps={},this.$inputor=t(e),this.setupRootElement(),this.listen()}return e.prototype.createContainer=function(e){var i;return null!=(i=this.$el)&&i.remove(),t(e.body).append(this.$el=t("
    "))},e.prototype.setupRootElement=function(e,i){var n,r;if(null==i&&(i=!1),e)this.window=e.contentWindow,this.document=e.contentDocument||this.window.document,this.iframe=e;else{this.document=this.$inputor[0].ownerDocument,this.window=this.document.defaultView||this.document.parentWindow;try{this.iframe=this.window.frameElement}catch(r){if(n=r,this.iframe=null,t.fn.atwho.debug)throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n"+n)}}return this.createContainer((this.iframeAsRoot=i)?this.document:document)},e.prototype.controller=function(t){var e,i,n,r;if(this.aliasMaps[t])i=this.controllers[this.aliasMaps[t]];else{r=this.controllers;for(n in r)if(e=r[n],n===t){i=e;break}}return i?i:this.controllers[this.currentFlag]},e.prototype.setContextFor=function(t){return this.currentFlag=t,this},e.prototype.reg=function(t,e){var i,n;return n=(i=this.controllers)[t]||(i[t]=this.$inputor.is("[contentEditable]")?new l(this,t):new s(this,t)),e.alias&&(this.aliasMaps[e.alias]=t),n.init(e),this},e.prototype.listen=function(){return this.$inputor.on("compositionstart",function(t){return function(e){var i;return null!=(i=t.controller())&&i.view.hide(),t.isComposing=!0,null}}(this)).on("compositionend",function(t){return function(e){return t.isComposing=!1,setTimeout(function(e){return t.dispatch(e)}),null}}(this)).on("keyup.atwhoInner",function(t){return function(e){return t.onKeyup(e)}}(this)).on("keydown.atwhoInner",function(t){return function(e){return t.onKeydown(e)}}(this)).on("blur.atwhoInner",function(t){return function(e){var i;return(i=t.controller())?(i.expectedQueryCBId=null,i.view.hide(e,i.getOpt("displayTimeout"))):void 0}}(this)).on("click.atwhoInner",function(t){return function(e){return t.dispatch(e)}}(this)).on("scroll.atwhoInner",function(t){return function(){var e;return e=t.$inputor.scrollTop(),function(i){var n,r;return n=i.target.scrollTop,e!==n&&null!=(r=t.controller())&&r.view.hide(i),e=n,!0}}}(this)())},e.prototype.shutdown=function(){var t,e,i;i=this.controllers;for(t in i)e=i[t],e.destroy(),delete this.controllers[t];return this.$inputor.off(".atwhoInner"),this.$el.remove()},e.prototype.dispatch=function(t){var e,i,n,r;if(void 0!==t){n=this.controllers,r=[];for(e in n)i=n[e],r.push(i.lookUp(t));return r}},e.prototype.onKeyup=function(e){var n;switch(e.keyCode){case i.ESC:e.preventDefault(),null!=(n=this.controller())&&n.view.hide();break;case i.DOWN:case i.UP:case i.CTRL:case i.ENTER:t.noop();break;case i.P:case i.N:e.ctrlKey||this.dispatch(e);break;default:this.dispatch(e)}},e.prototype.onKeydown=function(e){var n,r;if(r=null!=(n=this.controller())?n.view:void 0,r&&r.visible())switch(e.keyCode){case i.ESC:e.preventDefault(),r.hide(e);break;case i.UP:e.preventDefault(),r.prev();break;case i.DOWN:e.preventDefault(),r.next();break;case i.P:if(!e.ctrlKey)return;e.preventDefault(),r.prev();break;case i.N:if(!e.ctrlKey)return;e.preventDefault(),r.next();break;case i.TAB:case i.ENTER:case i.SPACE:if(!r.visible())return;if(!this.controller().getOpt("spaceSelectsMatch")&&e.keyCode===i.SPACE)return;if(!this.controller().getOpt("tabSelectsMatch")&&e.keyCode===i.TAB)return;r.highlighted()?(e.preventDefault(),r.choose(e)):r.hide(e);break;default:t.noop()}},e}();var r,o=[].slice;r=function(){function i(e,i){this.app=e,this.at=i,this.$inputor=this.app.$inputor,this.id=this.$inputor[0].id||this.uid(),this.expectedQueryCBId=null,this.setting=null,this.query=null,this.pos=0,this.range=null,0===(this.$el=t("#atwho-ground-"+this.id,this.app.$el)).length&&this.app.$el.append(this.$el=t("
    ")),this.model=new u(this),this.view=new c(this)}return i.prototype.uid=function(){return(Math.random().toString(16)+"000000000").substr(2,8)+(new Date).getTime()},i.prototype.init=function(e){return this.setting=t.extend({},this.setting||t.fn.atwho["default"],e),this.view.init(),this.model.reload(this.setting.data)},i.prototype.destroy=function(){return this.trigger("beforeDestroy"),this.model.destroy(),this.view.destroy(),this.$el.remove()},i.prototype.callDefault=function(){var i,n,r,s;s=arguments[0],i=2<=arguments.length?o.call(arguments,1):[];try{return e[s].apply(this,i)}catch(r){return n=r,t.error(n+" Or maybe At.js doesn't have function "+s)}},i.prototype.trigger=function(t,e){var i,n;return null==e&&(e=[]),e.push(this),i=this.getOpt("alias"),n=i?t+"-"+i+".atwho":t+".atwho",this.$inputor.trigger(n,e)},i.prototype.callbacks=function(t){return this.getOpt("callbacks")[t]||e[t]},i.prototype.getOpt=function(t,e){var i,n;try{return this.setting[t]}catch(n){return i=n,null}},i.prototype.insertContentFor=function(e){var i,n;return n=this.getOpt("insertTpl"),i=t.extend({},e.data("item-data"),{"atwho-at":this.at}),this.callbacks("tplEval").call(this,n,i,"onInsert")},i.prototype.renderView=function(t){var e;return e=this.getOpt("searchKey"),t=this.callbacks("sorter").call(this,this.query.text,t.slice(0,1001),e),this.view.render(t.slice(0,this.getOpt("limit")))},i.arrayToDefaultHash=function(e){var i,n,r,o;if(!t.isArray(e))return e;for(o=[],i=0,r=e.length;r>i;i++)n=e[i],t.isPlainObject(n)?o.push(n):o.push({name:n});return o},i.prototype.lookUp=function(t){var e,i;if((!t||"click"!==t.type||this.getOpt("lookUpOnClick"))&&(!this.getOpt("suspendOnComposing")||!this.app.isComposing))return(e=this.catchQuery(t))?(this.app.setContextFor(this.at),(i=this.getOpt("delay"))?this._delayLookUp(e,i):this._lookUp(e),e):(this.expectedQueryCBId=null,e)},i.prototype._delayLookUp=function(t,e){var i,n;return i=Date.now?Date.now():(new Date).getTime(),this.previousCallTime||(this.previousCallTime=i),n=e-(i-this.previousCallTime),n>0&&e>n?(this.previousCallTime=i,this._stopDelayedCall(),this.delayedCallTimeout=setTimeout(function(e){return function(){return e.previousCallTime=0,e.delayedCallTimeout=null,e._lookUp(t)}}(this),e)):(this._stopDelayedCall(),this.previousCallTime!==i&&(this.previousCallTime=0),this._lookUp(t))},i.prototype._stopDelayedCall=function(){return this.delayedCallTimeout?(clearTimeout(this.delayedCallTimeout),this.delayedCallTimeout=null):void 0},i.prototype._generateQueryCBId=function(){return{}},i.prototype._lookUp=function(e){var i;return i=function(t,e){return t===this.expectedQueryCBId?e&&e.length>0?this.renderView(this.constructor.arrayToDefaultHash(e)):this.view.hide():void 0},this.expectedQueryCBId=this._generateQueryCBId(),this.model.query(e.text,t.proxy(i,this,this.expectedQueryCBId))},i}();var s,a=function(t,e){function i(){this.constructor=t}for(var n in e)h.call(e,n)&&(t[n]=e[n]);return i.prototype=e.prototype,t.prototype=new i,t.__super__=e.prototype,t},h={}.hasOwnProperty;s=function(e){function i(){return i.__super__.constructor.apply(this,arguments)}return a(i,e),i.prototype.catchQuery=function(){var t,e,i,n,r,o,s;return e=this.$inputor.val(),t=this.$inputor.caret("pos",{iframe:this.app.iframe}),s=e.slice(0,t),r=this.callbacks("matcher").call(this,this.at,s,this.getOpt("startWithSpace"),this.getOpt("acceptSpaceBar")),n="string"==typeof r,n&&r.length0?t.getRangeAt(0):void 0},n.prototype._setRange=function(e,i,n){return null==n&&(n=this._getRange()),n&&i?(i=t(i)[0],"after"===e?(n.setEndAfter(i),n.setStartAfter(i)):(n.setEndBefore(i),n.setStartBefore(i)),n.collapse(!1),this._clearRange(n)):void 0},n.prototype._clearRange=function(t){var e;return null==t&&(t=this._getRange()),e=this.app.window.getSelection(),null==this.ctrl_a_pressed?(e.removeAllRanges(),e.addRange(t)):void 0},n.prototype._movingEvent=function(t){var e;return"click"===t.type||(e=t.which)===i.RIGHT||e===i.LEFT||e===i.UP||e===i.DOWN},n.prototype._unwrap=function(e){var i;return e=t(e).unwrap().get(0),(i=e.nextSibling)&&i.nodeValue&&(e.nodeValue+=i.nodeValue,t(i).remove()),e},n.prototype.catchQuery=function(e){var n,r,o,s,a,h,l,u,c,p,f,d;if((d=this._getRange())&&d.collapsed){if(e.which===i.ENTER)return(r=t(d.startContainer).closest(".atwho-query")).contents().unwrap(),r.is(":empty")&&r.remove(),(r=t(".atwho-query",this.app.document)).text(r.text()).contents().last().unwrap(),void this._clearRange();if(/firefox/i.test(navigator.userAgent)){if(t(d.startContainer).is(this.$inputor))return void this._clearRange();e.which===i.BACKSPACE&&d.startContainer.nodeType===document.ELEMENT_NODE&&(c=d.startOffset-1)>=0?(o=d.cloneRange(),o.setStart(d.startContainer,c),t(o.cloneContents()).contents().last().is(".atwho-inserted")&&(a=t(d.startContainer).contents().get(c),this._setRange("after",t(a).contents().last()))):e.which===i.LEFT&&d.startContainer.nodeType===document.TEXT_NODE&&(n=t(d.startContainer.previousSibling),n.is(".atwho-inserted")&&0===d.startOffset&&this._setRange("after",n.contents().last()))}if(t(d.startContainer).closest(".atwho-inserted").addClass("atwho-query").siblings().removeClass("atwho-query"),(r=t(".atwho-query",this.app.document)).length>0&&r.is(":empty")&&0===r.text().length&&r.remove(),this._movingEvent(e)||r.removeClass("atwho-inserted"),r.length>0)switch(e.which){case i.LEFT:return this._setRange("before",r.get(0),d),void r.removeClass("atwho-query");case i.RIGHT:return this._setRange("after",r.get(0).nextSibling,d),void r.removeClass("atwho-query")}if(r.length>0&&(f=r.attr("data-atwho-at-query"))&&(r.empty().html(f).attr("data-atwho-at-query",null),this._setRange("after",r.get(0),d)),o=d.cloneRange(),o.setStart(d.startContainer,0),u=this.callbacks("matcher").call(this,this.at,o.toString(),this.getOpt("startWithSpace"),this.getOpt("acceptSpaceBar")),h="string"==typeof u,0===r.length&&h&&(s=d.startOffset-this.at.length-u.length)>=0&&(d.setStart(d.startContainer,s),r=t("",this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass("atwho-query"),d.surroundContents(r.get(0)),l=r.contents().last().get(0),l&&(/firefox/i.test(navigator.userAgent)?(d.setStart(l,l.length),d.setEnd(l,l.length),this._clearRange(d)):this._setRange("after",l,d))),!(h&&u.length=0&&(this._movingEvent(e)&&r.hasClass("atwho-inserted")?r.removeClass("atwho-query"):!1!==this.callbacks("afterMatchFailed").call(this,this.at,r)&&this._setRange("after",this._unwrap(r.text(r.text()).contents().first()))),null)}},n.prototype.rect=function(){var e,i,n;return n=this.query.el.offset(),n&&this.query.el[0].getClientRects().length?(this.app.iframe&&!this.app.iframeAsRoot&&(i=(e=t(this.app.iframe)).offset(),n.left+=i.left-this.$inputor.scrollLeft(),n.top+=i.top-this.$inputor.scrollTop()),n.bottom=n.top+this.query.el.height(),n):void 0},n.prototype.insert=function(t,e){var i,n,r,o,s;return this.$inputor.is(":focus")||this.$inputor.focus(),n=this.getOpt("functionOverrides"),n.insert?n.insert.call(this,t,e):(o=""===(o=this.getOpt("suffix"))?o:o||" ",i=e.data("item-data"),this.query.el.removeClass("atwho-query").addClass("atwho-inserted").html(t).attr("data-atwho-at-query",""+i["atwho-at"]+this.query.text).attr("contenteditable","false"),(r=this._getRange())&&(this.query.el.length&&r.setEndAfter(this.query.el[0]),r.collapse(!1),r.insertNode(s=this.app.document.createTextNode(""+o)),this._setRange("after",s,r)),this.$inputor.is(":focus")||this.$inputor.focus(),this.$inputor.change())},n}(r);var u;u=function(){function e(t){this.context=t,this.at=this.context.at,this.storage=this.context.$inputor}return e.prototype.destroy=function(){return this.storage.data(this.at,null)},e.prototype.saved=function(){return this.fetch()>0},e.prototype.query=function(t,e){var i,n,r;return n=this.fetch(),r=this.context.getOpt("searchKey"),n=this.context.callbacks("filter").call(this.context,t,n,r)||[],i=this.context.callbacks("remoteFilter"),n.length>0||!i&&0===n.length?e(n):i.call(this.context,t,e)},e.prototype.fetch=function(){return this.storage.data(this.at)||[]},e.prototype.save=function(t){return this.storage.data(this.at,this.context.callbacks("beforeSave").call(this.context,t||[]))},e.prototype.load=function(t){return!this.saved()&&t?this._load(t):void 0},e.prototype.reload=function(t){return this._load(t)},e.prototype._load=function(e){return"string"==typeof e?t.ajax(e,{dataType:"json"}).done(function(t){return function(e){return t.save(e)}}(this)):this.save(e)},e}();var c;c=function(){function e(e){this.context=e,this.$el=t("
      "),this.$elUl=this.$el.children(),this.timeoutID=null,this.context.$el.append(this.$el),this.bindEvent()}return e.prototype.init=function(){var t,e;return e=this.context.getOpt("alias")||this.context.at.charCodeAt(0),t=this.context.getOpt("headerTpl"),t&&1===this.$el.children().length&&this.$el.prepend(t),this.$el.attr({id:"at-view-"+e})},e.prototype.destroy=function(){return this.$el.remove()},e.prototype.bindEvent=function(){var e,i,n;return e=this.$el.find("ul"),i=0,n=0,e.on("mousemove.atwho-view","li",function(r){return function(r){var o;if((i!==r.clientX||n!==r.clientY)&&(i=r.clientX,n=r.clientY,o=t(r.currentTarget),!o.hasClass("cur")))return e.find(".cur").removeClass("cur"),o.addClass("cur")}}(this)).on("click.atwho-view","li",function(i){return function(n){return e.find(".cur").removeClass("cur"),t(n.currentTarget).addClass("cur"),i.choose(n),n.preventDefault()}}(this))},e.prototype.visible=function(){return t.expr.filters.visible(this.$el[0])},e.prototype.highlighted=function(){return this.$el.find(".cur").length>0},e.prototype.choose=function(t){var e,i;return(e=this.$el.find(".cur")).length&&(i=this.context.insertContentFor(e),this.context._stopDelayedCall(),this.context.insert(this.context.callbacks("beforeInsert").call(this.context,i,e,t),e),this.context.trigger("inserted",[e,t]),this.hide(t)),this.context.getOpt("hideWithoutSuffix")?this.stopShowing=!0:void 0},e.prototype.reposition=function(e){var i,n,r,o;return i=this.context.app.iframeAsRoot?this.context.app.window:window,e.bottom+this.$el.height()-t(i).scrollTop()>t(i).height()&&(e.bottom=e.top-this.$el.height()),e.left>(r=t(i).width()-this.$el.width()-5)&&(e.left=r),n={left:e.left,top:e.bottom},null!=(o=this.context.callbacks("beforeReposition"))&&o.call(this.context,n),this.$el.offset(n),this.context.trigger("reposition",[n])},e.prototype.next=function(){var t,e,i,n;return t=this.$el.find(".cur").removeClass("cur"),e=t.next(),e.length||(e=this.$el.find("li:first")),e.addClass("cur"),i=e[0],n=i.offsetTop+i.offsetHeight+(i.nextSibling?i.nextSibling.offsetHeight:0),this.scrollTop(Math.max(0,n-this.$el.height()))},e.prototype.prev=function(){var t,e,i,n;return t=this.$el.find(".cur").removeClass("cur"),i=t.prev(),i.length||(i=this.$el.find("li:last")),i.addClass("cur"),n=i[0],e=n.offsetTop+n.offsetHeight+(n.nextSibling?n.nextSibling.offsetHeight:0),this.scrollTop(Math.max(0,e-this.$el.height()))},e.prototype.scrollTop=function(t){var e;return e=this.context.getOpt("scrollDuration"),e?this.$elUl.animate({scrollTop:t},e):this.$elUl.scrollTop(t)},e.prototype.show=function(){var t;return this.stopShowing?void(this.stopShowing=!1):(this.visible()||(this.$el.show(),this.$el.scrollTop(0),this.context.trigger("shown")),(t=this.context.rect())?this.reposition(t):void 0)},e.prototype.hide=function(t,e){var i;if(this.visible())return isNaN(e)?(this.$el.hide(),this.context.trigger("hidden",[t])):(i=function(t){return function(){return t.hide()}}(this),clearTimeout(this.timeoutID),this.timeoutID=setTimeout(i,e))},e.prototype.render=function(e){var i,n,r,o,s,a,h;if(!(t.isArray(e)&&e.length>0))return void this.hide();for(this.$el.find("ul").empty(),n=this.$el.find("ul"),h=this.context.getOpt("displayTpl"),r=0,s=e.length;s>r;r++)o=e[r],o=t.extend({},o,{"atwho-at":this.context.at}),a=this.context.callbacks("tplEval").call(this.context,h,o,"onDisplay"),i=t(this.context.callbacks("highlighter").call(this.context,a,this.context.query.text)),i.data("item-data",o),n.append(i);return this.show(),this.context.getOpt("highlightFirst")?n.find("li:first").addClass("cur"):void 0},e}();var p;p={load:function(t,e){var i;return(i=this.controller(t))?i.model.load(e):void 0},isSelecting:function(){var t;return!!(null!=(t=this.controller())?t.view.visible():void 0)},hide:function(){var t;return null!=(t=this.controller())?t.view.hide():void 0},reposition:function(){var t;return(t=this.controller())?t.view.reposition(t.rect()):void 0},setIframe:function(t,e){return this.setupRootElement(t,e),null},run:function(){return this.dispatch()},destroy:function(){return this.shutdown(),this.$inputor.data("atwho",null)}},t.fn.atwho=function(e){var i,r;return i=arguments,r=null,this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function(){var o,s;return(s=(o=t(this)).data("atwho"))||o.data("atwho",s=new n(this)),"object"!=typeof e&&e?p[e]&&s?r=p[e].apply(s,Array.prototype.slice.call(i,1)):t.error("Method "+e+" does not exist on jQuery.atwho"):s.reg(e.at,e)}),null!=r?r:this},t.fn.atwho["default"]={at:void 0,alias:void 0,data:null,displayTpl:"
    • ${name}
    • ",insertTpl:"${atwho-at}${name}",headerTpl:null,callbacks:e,functionOverrides:{},searchKey:"name",suffix:void 0,hideWithoutSuffix:!1,startWithSpace:!0,acceptSpaceBar:!1,highlightFirst:!0,limit:5,maxLen:20,minLen:0,displayTimeout:300,delay:null,spaceSelectsMatch:!1,tabSelectsMatch:!0,editableAtwhoQueryAttrs:{},scrollDuration:150,suspendOnComposing:!0,lookUpOnClick:!0},t.fn.atwho.debug=!1}); \ No newline at end of file From 352f3bd1527b8f7c1f9d58e38cfc5a04c9d0c451 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Mon, 18 Aug 2025 16:00:04 -0600 Subject: [PATCH 6/6] Correctly handles quotes in mentions Signed-off-by: Jon Stovell --- Sources/Mentions.php | 65 ++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/Sources/Mentions.php b/Sources/Mentions.php index e9a5e81ebb..6d095a1714 100644 --- a/Sources/Mentions.php +++ b/Sources/Mentions.php @@ -237,7 +237,7 @@ public static function getMentionedMembers(string $body): array LIMIT {int:count}', [ 'ids' => array_keys($existing_mentions), - 'names' => $possible_names, + 'names' => Utils::htmlspecialcharsRecursive($possible_names, ENT_QUOTES, double_encode: false), 'count' => count($possible_names), ], ); @@ -258,33 +258,6 @@ public static function getMentionedMembers(string $body): array return $members; } - /** - * Like getPossibleMentions(), but for `[member=1]name[/member]` format. - * - * @static - * @param string $body The text to look for mentions in. - * @return array An array of arrays containing info about members that are in fact mentioned in the body. - */ - public static function getExistingMentions(string $body): array - { - if (empty(self::$excluded_bbc_regex)) { - self::setExcludedBbcRegex(); - } - - // Don't include mentions inside quotations, etc. - $body = preg_replace('~\[(' . self::$excluded_bbc_regex . ')[^\]]*\](?' . '>(?' . '>[^\[]|\[(?!/?\1[^\]]*\]))|(?0))*\[/\1\]~', '', $body); - - $existing_mentions = []; - - preg_match_all('~\[member=([0-9]+)\]([^\[]*)\[/member\]~', $body, $matches, PREG_SET_ORDER); - - foreach ($matches as $match_set) { - $existing_mentions[$match_set[1]] = trim($match_set[2]); - } - - return $existing_mentions; - } - /** * Verifies that members really are mentioned in the text. * @@ -429,7 +402,7 @@ protected static function getPossibleMentions(string $body): array } // preparse code does a few things which might mess with our parsing - $body = htmlspecialchars_decode(preg_replace('~~', "\n", str_replace(' ', ' ', $body)), ENT_QUOTES); + $body = Utils::htmlspecialcharsDecode(preg_replace('~]*>~', "\n", $body), ENT_QUOTES); if (empty(self::$excluded_bbc_regex)) { self::setExcludedBbcRegex(); @@ -440,7 +413,7 @@ protected static function getPossibleMentions(string $body): array $matches = []; // Split before every Unicode character. - $string = preg_split('/(?=\X)/u', $body, -1, PREG_SPLIT_NO_EMPTY); + $string = Utils::entityStrSplit($body); $depth = 0; foreach ($string as $k => $char) { @@ -470,12 +443,11 @@ protected static function getPossibleMentions(string $body): array $names = []; foreach ($matches as $match) { - // '[^\p{L}\p{M}\p{N}_]' is the Unicode equivalent of '[^\w]' - $match = preg_split('/([^\p{L}\p{M}\p{N}_])/u', $match, -1, PREG_SPLIT_DELIM_CAPTURE); + $match = Utils::entityStrSplit($match); $count = count($match); for ($i = 1; $i <= $count; $i++) { - $names[] = Utils::htmlspecialchars(Utils::htmlTrim(implode('', array_slice($match, 0, $i)))); + $names[] = Utils::htmlspecialchars(Utils::htmlTrim(implode('', array_slice($match, 0, $i))), ENT_COMPAT); } } @@ -484,6 +456,33 @@ protected static function getPossibleMentions(string $body): array return $names; } + /** + * Like getPossibleMentions(), but for `[member=1]name[/member]` format. + * + * @static + * @param string $body The text to look for mentions in. + * @return array An array of arrays containing info about members that are in fact mentioned in the body. + */ + protected static function getExistingMentions(string $body): array + { + if (empty(self::$excluded_bbc_regex)) { + self::setExcludedBbcRegex(); + } + + // Don't include mentions inside quotations, etc. + $body = preg_replace('~\[(' . self::$excluded_bbc_regex . ')[^\]]*\](?' . '>(?' . '>[^\[]|\[(?!/?\1[^\]]*\]))|(?0))*\[/\1\]~', '', $body); + + $existing_mentions = []; + + preg_match_all('~\[member=([0-9]+)\]([^\[]*)\[/member\]~', $body, $matches, PREG_SET_ORDER); + + foreach ($matches as $match_set) { + $existing_mentions[$match_set[1]] = Utils::htmlspecialchars(Utils::htmlTrim(Utils::htmlspecialcharsDecode($match_set[2], ENT_QUOTES)), ENT_COMPAT); + } + + return $existing_mentions; + } + /** * Builds a regular expression matching BBC that can't contain mentions. *