diff --git a/Sources/Autolinker.php b/Sources/Autolinker.php index 6d3b370a2b..3208667b54 100644 --- a/Sources/Autolinker.php +++ b/Sources/Autolinker.php @@ -217,6 +217,13 @@ class Autolinker */ protected string $js_email_regex; + /** + * @var string + * + * Regular expression to match the named entities in HTML5. + */ + protected string $entities_regex; + /**************************** * Internal static properties ****************************/ @@ -377,10 +384,9 @@ public function detectUrls(string $string, bool $plaintext_only = false): array { static $no_autolink_regex; - // An   right after a URL can break the autolinker - if (str_contains($string, ' ')) { - $string = strtr($string, [' ' => str_repeat(html_entity_decode(' ', 0, $this->encoding), 3)]); - } + // An entity right after the URL can break the autolinker. + $this->setEntitiesRegex(); + $string = preg_replace('~(' . $this->entities_regex . ')*(?=\s|$)~u', ' ', $string); $this->setUrlRegex(); @@ -467,10 +473,9 @@ public function detectUrls(string $string, bool $plaintext_only = false): array */ public function detectEmails(string $string, bool $plaintext_only = false): array { - // An   right after a email address can break the autolinker - if (str_contains($string, ' ')) { - $string = strtr($string, [' ' => str_repeat(html_entity_decode(' ', 0, $this->encoding), 3)]); - } + // An entity right after the email address can break the autolinker. + $this->setEntitiesRegex(); + $string = preg_replace('~(' . $this->entities_regex . ')*(?=\s|$)~u', ' ', $string); $this->setEmailRegex(); @@ -758,10 +763,63 @@ public static function load(bool $only_basic = false): object return self::$instance; } + /** + * Creates the JavaScript file used for autolinking in the editor. + * + * @param bool $force Whether to overwrite an existing file. Default: false. + */ + public static function createJavaScriptFile(bool $force = false): void + { + if (empty(Config::$modSettings['autoLinkUrls'])) { + return; + } + + if (!isset(Theme::$current)) { + Theme::loadEssential(); + } + + if (!$force && file_exists(Theme::$current->settings['default_theme_dir'] . '/scripts/autolinker.js')) { + return; + } + + $js[] = 'const autolinker_regexes = new Map();'; + + $regexes = self::load()->getJavaScriptUrlRegexes(); + $regexes['email'] = self::load()->getJavaScriptEmailRegex(); + + foreach ($regexes as $key => $value) { + $js[] = 'autolinker_regexes.set(' . Utils::escapeJavaScript($key) . ', new RegExp(' . Utils::escapeJavaScript($value) . ', "giu"));'; + + $js[] = 'autolinker_regexes.set(' . Utils::escapeJavaScript('paste_' . $key) . ', new RegExp(' . Utils::escapeJavaScript('(?<=^|\s|
)' . $value . '(?=$|\s|
|[' . self::$excluded_trailing_chars . '])') . ', "giu"));'; + + $js[] = 'autolinker_regexes.set(' . Utils::escapeJavaScript('keypress_' . $key) . ', new RegExp(' . Utils::escapeJavaScript($value . '(?=[' . self::$excluded_trailing_chars . preg_quote(implode(array_merge(array_keys(self::$balanced_pairs), self::$balanced_pairs)), '/') . ']*\s$)') . ', "giu"));'; + } + + $js[] = 'const autolinker_balanced_pairs = new Map();'; + + foreach (self::$balanced_pairs as $opener => $closer) { + $js[] = 'autolinker_balanced_pairs.set(' . Utils::escapeJavaScript($opener) . ', ' . Utils::escapeJavaScript($closer) . ');'; + } + + file_put_contents(Theme::$current->settings['default_theme_dir'] . '/scripts/autolinker.js', implode("\n", $js)); + } + /******************* * Internal methods. *******************/ + /** + * Sets $this->entities_regex. + */ + protected function setEntitiesRegex(): void + { + if (isset($this->entities_regex)) { + return; + } + + $this->entities_regex = '(?' . '>&(?' . '>' . Utils::buildRegex(array_map(fn ($ent) => ltrim($ent, '&'), get_html_translation_table(HTML_ENTITIES, ENT_HTML5 | ENT_QUOTES)), '~') . '|(?' . '>#(?' . '>x[0-9a-fA-F]{1,6}|\d{1,7});)))'; + } + /** * Sets $this->tld_regex. */ diff --git a/Sources/Editor.php b/Sources/Editor.php index d4d4274b14..7266e9b2f7 100644 --- a/Sources/Editor.php +++ b/Sources/Editor.php @@ -227,7 +227,7 @@ public function __construct(array $options) $this->columns = (int) ($options['columns'] ?? 60); $this->rows = (int) ($options['rows'] ?? 18); $this->width = (string) ($options['width'] ?? '70%'); - $this->height = (string) ($options['height'] ?? '175px'); + $this->height = (string) ($options['height'] ?? '250px'); $this->form = (string) ($options['form'] ?? 'postmodify'); $this->preview_type = (int) ($options['preview_type'] ?? self::PREVIEW_HTML); $this->labels = (array) ($options['labels'] ?? []); @@ -775,34 +775,19 @@ protected function setSCEditorOptions(): void // Set up the SCEditor options $this->sce_options = [ 'width' => $this->width ?? '100%', - 'height' => $this->height ?? '175px', + 'height' => $this->height ?? '250px', 'style' => Theme::$current->settings[file_exists(Theme::$current->settings['theme_dir'] . '/css/jquery.sceditor.default.css') ? 'theme_url' : 'default_theme_url'] . '/css/jquery.sceditor.default.css' . Utils::$context['browser_cache'], 'emoticonsCompat' => true, 'colors' => 'black,maroon,brown,green,navy,grey,red,orange,teal,blue,white,hotpink,yellow,limegreen,purple', 'format' => 'bbcode', - 'plugins' => !empty(Config::$modSettings['autoLinkUrls']) ? 'autolinker' : '', + 'plugins' => '', 'bbcodeTrim' => false, ]; if (!empty(Config::$modSettings['autoLinkUrls'])) { - $js = "\n\t\t" . 'const autolinker_regexes = new Map();'; - - $regexes = Autolinker::load()->getJavaScriptUrlRegexes(); - $regexes['email'] = Autolinker::load()->getJavaScriptEmailRegex(); - - foreach ($regexes as $key => $value) { - $js .= "\n\t\t" . 'autolinker_regexes.set(' . Utils::escapeJavaScript($key) . ', new RegExp(' . Utils::escapeJavaScript($value) . ', "giu"));'; - $js .= "\n\t\t" . 'autolinker_regexes.set(' . Utils::escapeJavaScript('paste_' . $key) . ', new RegExp(' . Utils::escapeJavaScript('(?<=^|\s|
)' . $value . '(?=$|\s|
|[' . Autolinker::$excluded_trailing_chars . '])') . ', "giu"));'; - $js .= "\n\t\t" . 'autolinker_regexes.set(' . Utils::escapeJavaScript('keypress_' . $key) . ', new RegExp(' . Utils::escapeJavaScript($value . '(?=[' . Autolinker::$excluded_trailing_chars . preg_quote(implode(array_merge(array_keys(Autolinker::$balanced_pairs), Autolinker::$balanced_pairs)), '/') . ']*\s$)') . ', "giu"));'; - } - - $js .= "\n\t\t" . 'const autolinker_balanced_pairs = new Map();'; - - foreach (Autolinker::$balanced_pairs as $opener => $closer) { - $js .= "\n\t\t" . 'autolinker_balanced_pairs.set(' . Utils::escapeJavaScript($opener) . ', ' . Utils::escapeJavaScript($closer) . ');'; - } - - Theme::addInlineJavaScript($js); + $this->sce_options['plugins'] = 'autolinker'; + Autolinker::createJavaScriptFile(); + Theme::loadJavaScriptFile('autolinker.js', ['minimize' => true], 'smf_autolinker'); } if (!empty($this->locale)) { diff --git a/Sources/Url.php b/Sources/Url.php index 32432b4e03..c06e8c3cec 100644 --- a/Sources/Url.php +++ b/Sources/Url.php @@ -753,6 +753,11 @@ function ($line) { // Remember the new regex in Config::$modSettings Config::updateModSettings(['tld_regex' => $tld_regex]); + // Update the editor's autolinker JavaScript. + if ($update) { + Autolinker::createJavaScriptFile(true); + } + // Redundant repetition is redundant $done = true; }