diff --git a/Sources/Actions/Admin/ACP.php b/Sources/Actions/Admin/ACP.php index afa160dd2f..a3fdc28abb 100644 --- a/Sources/Actions/Admin/ACP.php +++ b/Sources/Actions/Admin/ACP.php @@ -19,7 +19,6 @@ use SMF\Actions\MessageIndex; use SMF\Actions\Notify; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Cache\CacheApi; use SMF\Config; use SMF\Db\DatabaseApi as Db; @@ -28,6 +27,7 @@ use SMF\Lang; use SMF\Mail; use SMF\Menu; +use SMF\Parser; use SMF\SecurityToken; use SMF\Theme; use SMF\Url; @@ -976,7 +976,7 @@ public static function prepareDBSettingContext(array &$config_vars): void // What about any BBC selection boxes? if (!empty($bbcChoice)) { // What are the options, eh? - $temp = BBCodeParser::getCodes(); + $temp = Parser::getBBCodes(); $bbcTags = []; foreach ($temp as $tag) { @@ -1351,7 +1351,7 @@ public static function saveDBSettings(array &$config_vars): void elseif ($var[0] == 'bbc') { $bbcTags = []; - foreach (BBCodeParser::getCodes() as $tag) { + foreach (Parser::getBBCodes() as $tag) { $bbcTags[] = $tag['tag']; } diff --git a/Sources/Actions/Admin/Boards.php b/Sources/Actions/Admin/Boards.php index 40c71fffcf..67b8ef4596 100644 --- a/Sources/Actions/Admin/Boards.php +++ b/Sources/Actions/Admin/Boards.php @@ -18,7 +18,6 @@ use SMF\ActionInterface; use SMF\Actions\BackwardCompatibility; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Board; use SMF\Category; use SMF\Config; @@ -28,6 +27,7 @@ use SMF\IntegrationHook; use SMF\Lang; use SMF\Menu; +use SMF\Parser; use SMF\SecurityToken; use SMF\Theme; use SMF\Url; @@ -366,7 +366,7 @@ public function editCategory2(): void // Try to get any valid HTML to BBC first, add a naive attempt to strip it off, htmlspecialchars for the rest $catOptions['cat_name'] = Utils::htmlspecialchars(strip_tags($_POST['cat_name'])); - $catOptions['cat_desc'] = Utils::htmlspecialchars(strip_tags(BBCodeParser::load()->unparse($_POST['cat_desc']))); + $catOptions['cat_desc'] = Utils::htmlspecialchars(strip_tags(Parser::transform($_POST['cat_desc'], Parser::OUTPUT_BBC))); $catOptions['is_collapsible'] = isset($_POST['collapse']); if (isset($_POST['add'])) { @@ -677,7 +677,7 @@ public function editBoard2(): void // Try to get any valid HTML to BBC first, add a naive attempt to strip it off, htmlspecialchars for the rest $boardOptions['board_name'] = Utils::htmlspecialchars(strip_tags($_POST['board_name'])); - $boardOptions['board_description'] = Utils::htmlspecialchars(strip_tags(BBCodeParser::load()->unparse($_POST['desc']))); + $boardOptions['board_description'] = Utils::htmlspecialchars(strip_tags(Parser::transform($_POST['desc'], Parser::OUTPUT_BBC))); $boardOptions['moderator_string'] = $_POST['moderators']; diff --git a/Sources/Actions/Admin/ErrorLog.php b/Sources/Actions/Admin/ErrorLog.php index b5a5664be9..ffd1018a3e 100644 --- a/Sources/Actions/Admin/ErrorLog.php +++ b/Sources/Actions/Admin/ErrorLog.php @@ -17,13 +17,13 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; use SMF\IP; use SMF\Lang; use SMF\PageIndex; +use SMF\Parser; use SMF\SecurityToken; use SMF\Theme; use SMF\Time; @@ -430,7 +430,7 @@ public function viewFile(): void ErrorHandler::fatalLang('error_bad_line'); } - $file_data = explode('
', BBCodeParser::highlightPhpCode(Utils::htmlspecialchars(file_get_contents($file)))); + $file_data = explode('
', Parser::highlightPhpCode(Utils::htmlspecialchars(file_get_contents($file)))); // We don't want to slice off too many so lets make sure we stop at the last one $max = min($max, max(array_keys($file_data))); diff --git a/Sources/Actions/Admin/Features.php b/Sources/Actions/Admin/Features.php index e7136c2c40..4483f665ab 100644 --- a/Sources/Actions/Admin/Features.php +++ b/Sources/Actions/Admin/Features.php @@ -19,7 +19,6 @@ use SMF\Actions\BackwardCompatibility; use SMF\Actions\Profile\Notification; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; @@ -27,8 +26,9 @@ use SMF\IntegrationHook; use SMF\ItemList; use SMF\Lang; -use SMF\MarkdownParser; use SMF\Menu; +use SMF\Parser; +use SMF\Parsers\MarkdownParser; use SMF\Profile; use SMF\Sapi; use SMF\SecurityToken; @@ -185,7 +185,7 @@ public function bbc(): void $bbcTags = []; $bbcTagsChildren = []; - foreach (BBCodeParser::getCodes() as $tag) { + foreach (Parser::getBBCodes() as $tag) { $bbcTags[] = $tag['tag']; if (isset($tag['require_children'])) { @@ -194,8 +194,8 @@ public function bbc(): void } // Clean up tags with children - foreach($bbcTagsChildren as $parent_tag => $children) { - foreach($children as $index => $child_tag) { + foreach ($bbcTagsChildren as $parent_tag => $children) { + foreach ($children as $index => $child_tag) { // Remove entries where parent and child tag is the same if ($child_tag == $parent_tag) { unset($bbcTagsChildren[$parent_tag][$index]); @@ -592,7 +592,7 @@ public function signature(): void // Clean up the tag stuff! $bbcTags = []; - foreach (BBCodeParser::getCodes() as $tag) { + foreach (Parser::getBBCodes() as $tag) { $bbcTags[] = $tag['tag']; } @@ -1035,7 +1035,7 @@ public function profileEdit(): void [], ); - while($row = Db::$db->fetch_assoc($request)) { + while ($row = Db::$db->fetch_assoc($request)) { $fields[] = $row['id_field']; } Db::$db->free_result($request); diff --git a/Sources/Actions/Admin/News.php b/Sources/Actions/Admin/News.php index 6f4c5fe5c6..7b141962f9 100644 --- a/Sources/Actions/Admin/News.php +++ b/Sources/Actions/Admin/News.php @@ -19,7 +19,6 @@ use SMF\Actions\BackwardCompatibility; use SMF\Actions\Notify; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\Editor; @@ -31,6 +30,7 @@ use SMF\Mail; use SMF\Menu; use SMF\Msg; +use SMF\Parser; use SMF\PersonalMessage\PM; use SMF\SecurityToken; use SMF\Theme; @@ -1106,7 +1106,7 @@ public static function list_getNews(): array $admin_current_news[$id] = [ 'id' => $id, 'unparsed' => Msg::un_preparsecode($line), - 'parsed' => preg_replace('~<([/]?)form[^>]*?[>]*>~i', '<$1form>', BBCodeParser::load()->parse($line)), + 'parsed' => preg_replace('~<([/]?)form[^>]*?[>]*>~i', '<$1form>', Parser::transform($line)), ]; } @@ -1193,7 +1193,7 @@ public static function prepareMailingForPreview(): void if (!empty(Utils::$context['send_html'])) { $enablePostHTML = Config::$modSettings['enablePostHTML']; Config::$modSettings['enablePostHTML'] = Utils::$context['send_html']; - Utils::$context[$key] = BBCodeParser::load()->parse(Utils::$context[$key]); + Utils::$context[$key] = Parser::transform(Utils::$context[$key]); Config::$modSettings['enablePostHTML'] = $enablePostHTML; } diff --git a/Sources/Actions/Admin/Smileys.php b/Sources/Actions/Admin/Smileys.php index f65ccd2eb8..5794751272 100644 --- a/Sources/Actions/Admin/Smileys.php +++ b/Sources/Actions/Admin/Smileys.php @@ -21,7 +21,6 @@ use SMF\Actions\BackwardCompatibility; use SMF\Actions\MessageIndex; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Cache\CacheApi; use SMF\Config; use SMF\Db\DatabaseApi as Db; @@ -33,6 +32,7 @@ use SMF\Menu; use SMF\Msg; use SMF\PackageManager\SubsPackage; +use SMF\Parser; use SMF\SecurityToken; use SMF\Theme; use SMF\User; @@ -1622,7 +1622,7 @@ public function install(): void if (!empty($action['parse_bbc'])) { Msg::preparsecode(Utils::$context[$type]); - Utils::$context[$type] = BBCodeParser::load()->parse(Utils::$context[$type]); + Utils::$context[$type] = Parser::transform(Utils::$context[$type]); } else { Utils::$context[$type] = nl2br(Utils::$context[$type]); } diff --git a/Sources/Actions/Agreement.php b/Sources/Actions/Agreement.php index 661886c250..be325340fe 100644 --- a/Sources/Actions/Agreement.php +++ b/Sources/Actions/Agreement.php @@ -17,11 +17,10 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Config; use SMF\ErrorHandler; use SMF\Lang; -use SMF\MarkdownParser; +use SMF\Parser; use SMF\Theme; use SMF\User; use SMF\Utils; @@ -149,31 +148,32 @@ protected function prepareAgreementContext(): void if (!empty(Utils::$context['agreement_file'])) { $cache_id = strtr(Utils::$context['agreement_file'], [Config::$languagesdir => '', '.txt' => '', '.' => '_']); - Utils::$context['agreement'] = BBCodeParser::load()->parse(file_get_contents(Utils::$context['agreement_file']), true, $cache_id); + Utils::$context['agreement'] = Parser::transform( + string: file_get_contents(Utils::$context['agreement_file']), + options: ['cache_id' => $cache_id, 'hard_breaks' => 0], + ); } elseif (Utils::$context['can_accept_agreement']) { ErrorHandler::fatalLang('error_no_agreement', false); } - - if (!empty(Config::$modSettings['enableMarkdown'])) { - Utils::$context['agreement'] = MarkdownParser::load(MarkdownParser::OUTPUT_HTML, 0)->parse(Utils::$context['agreement'], true); - } } if (!Utils::$context['accept_doc'] || Utils::$context['can_accept_privacy_policy']) { // Have we got a localized policy? if (!empty(Config::$modSettings['policy_' . User::$me->language])) { - Utils::$context['privacy_policy'] = BBCodeParser::load()->parse(Config::$modSettings['policy_' . User::$me->language]); + Utils::$context['privacy_policy'] = Parser::transform( + string: Config::$modSettings['policy_' . User::$me->language], + options: ['hard_breaks' => 0], + ); } elseif (!empty(Config::$modSettings['policy_' . Lang::$default])) { - Utils::$context['privacy_policy'] = BBCodeParser::load()->parse(Config::$modSettings['policy_' . Lang::$default]); + Utils::$context['privacy_policy'] = Parser::transform( + string: Config::$modSettings['policy_' . Lang::$default], + options: ['hard_breaks' => 0], + ); } // Then I guess we've got nothing elseif (Utils::$context['can_accept_privacy_policy']) { ErrorHandler::fatalLang('error_no_privacy_policy', false); } - - if (!empty(Config::$modSettings['enableMarkdown'])) { - Utils::$context['privacy_policy'] = MarkdownParser::load(MarkdownParser::OUTPUT_HTML, 0)->parse(Utils::$context['privacy_policy'], true); - } } } } diff --git a/Sources/Actions/Announce.php b/Sources/Actions/Announce.php index 4768a6fda4..2185bc09c8 100644 --- a/Sources/Actions/Announce.php +++ b/Sources/Actions/Announce.php @@ -17,7 +17,6 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Board; use SMF\BrowserDetector; use SMF\Config; @@ -27,7 +26,7 @@ use SMF\Lang; use SMF\Logging; use SMF\Mail; -use SMF\MarkdownParser; +use SMF\Parser; use SMF\Theme; use SMF\Topic; use SMF\User; @@ -168,11 +167,7 @@ public function send(): void Lang::censorText(Utils::$context['topic_subject']); Lang::censorText($message); - $message = BBCodeParser::load()->parse($message, false, $id_msg); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $message = MarkdownParser::load()->parse($message, true); - } + $message = Parser::transform(string: $message, options: ['cache_id' => $id_msg]); $message = trim(Utils::htmlspecialcharsDecode(strip_tags(strtr($message, ['
' => "\n", '' => "\n", '' => "\n", '

' => '', '

' => "\n\n", '[' => '[', ']' => ']'])))); diff --git a/Sources/Actions/Feed.php b/Sources/Actions/Feed.php index abb2d3b327..df8b8598e0 100644 --- a/Sources/Actions/Feed.php +++ b/Sources/Actions/Feed.php @@ -19,7 +19,6 @@ use SMF\ActionTrait; use SMF\Attachment; use SMF\Autolinker; -use SMF\BBCodeParser; use SMF\Board; use SMF\BrowserDetector; use SMF\Cache\CacheApi; @@ -29,7 +28,7 @@ use SMF\IntegrationHook; use SMF\IP; use SMF\Lang; -use SMF\MarkdownParser; +use SMF\Parser; use SMF\Sapi; use SMF\Theme; use SMF\Time; @@ -779,11 +778,11 @@ public function getXmlNews(): array $row['body'] = strtr(Utils::entitySubstr(str_replace('
', "\n", $row['body']), 0, Config::$modSettings['xmlnews_maxlen'] - 3), ["\n" => '
']) . '...'; } - $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], (int) $row['id_msg']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } + $row['body'] = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => (int) $row['id_msg']], + ); Lang::censorText($row['body']); Lang::censorText($row['subject']); @@ -1223,11 +1222,11 @@ public function getXmlRecent(): array $row['body'] = strtr(Utils::entitySubstr(str_replace('
', "\n", $row['body']), 0, Config::$modSettings['xmlnews_maxlen'] - 3), ["\n" => '
']) . '...'; } - $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], (int) $row['id_msg']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } + $row['body'] = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => (int) $row['id_msg']], + ); Lang::censorText($row['body']); Lang::censorText($row['subject']); @@ -1991,11 +1990,11 @@ public function getXmlPosts(): array } // If using our own format, we want both the raw and the parsed content. - $row[$this->format === 'smf' ? 'body_html' : 'body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], (int) $row['id_msg']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row[$this->format === 'smf' ? 'body_html' : 'body'] = MarkdownParser::load()->parse($row[$this->format === 'smf' ? 'body_html' : 'body'], true); - } + $row[$this->format === 'smf' ? 'body_html' : 'body'] = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => (int) $row['id_msg']], + ); // Do we want to include any attachments? if (!empty(Config::$modSettings['attachmentEnable']) && !empty(Config::$modSettings['xmlnews_attachments'])) { @@ -2446,11 +2445,7 @@ public function getXmlPMs(): array } // If using our own format, we want both the raw and the parsed content. - $row[$this->format === 'smf' ? 'body_html' : 'body'] = BBCodeParser::load()->parse($row['body']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row[$this->format === 'smf' ? 'body_html' : 'body'] = MarkdownParser::load()->parse($row[$this->format === 'smf' ? 'body_html' : 'body'], true); - } + $row[$this->format === 'smf' ? 'body_html' : 'body'] = Parser::transform($row['body']); $recipients = array_combine(explode(',', $row['id_members_to']), explode($separator, $row['to_names'])); diff --git a/Sources/Actions/Groups.php b/Sources/Actions/Groups.php index 6f0b9ddb32..0293bf7071 100644 --- a/Sources/Actions/Groups.php +++ b/Sources/Actions/Groups.php @@ -16,7 +16,6 @@ use SMF\ActionInterface; use SMF\Actions\Moderation\Main as ModCenter; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; @@ -26,6 +25,7 @@ use SMF\Lang; use SMF\Menu; use SMF\PageIndex; +use SMF\Parser; use SMF\SecurityToken; use SMF\Theme; use SMF\Time; @@ -734,7 +734,11 @@ public static function list_getMembergroups($start, $items_per_page, $sort, $mem Utils::$context['can_moderate'] |= $group->can_moderate; - $group->description = BBCodeParser::load()->parse($group->description, false, '', Utils::$context['description_allowed_tags']); + $group->description = Parser::transform( + string: $group->description, + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN, + options: ['parse_tags' => Utils::$context['description_allowed_tags']], + ); $groups[$group->id] = $group; $group_ids[] = $group->id; diff --git a/Sources/Actions/JavaScriptModify.php b/Sources/Actions/JavaScriptModify.php index c37f1fcfed..4f578672f1 100644 --- a/Sources/Actions/JavaScriptModify.php +++ b/Sources/Actions/JavaScriptModify.php @@ -18,7 +18,6 @@ use SMF\ActionInterface; use SMF\ActionTrait; use SMF\Autolinker; -use SMF\BBCodeParser; use SMF\Board; use SMF\Cache\CacheApi; use SMF\Config; @@ -27,8 +26,8 @@ use SMF\IntegrationHook; use SMF\Lang; use SMF\Logging; -use SMF\MarkdownParser; use SMF\Msg; +use SMF\Parser; use SMF\Time; use SMF\Topic; use SMF\User; @@ -138,11 +137,10 @@ public function execute(): void Msg::preparsecode($_POST['message']); - $temp = BBCodeParser::load()->parse($_POST['message'], false); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $temp = MarkdownParser::load()->parse($temp, true); - } + $temp = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN, + ); if (Utils::htmlTrim(strip_tags($temp, implode('', Utils::$context['allowed_html_tags']))) === '') { $post_errors[] = 'no_message'; @@ -301,11 +299,11 @@ public function execute(): void Lang::censorText(Utils::$context['message']['subject']); Lang::censorText(Utils::$context['message']['body']); - Utils::$context['message']['body'] = BBCodeParser::load()->parse(Utils::$context['message']['body'], (bool) $row['smileys_enabled'], (int) $row['id_msg']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - Utils::$context['message']['body'] = MarkdownParser::load()->parse(Utils::$context['message']['body'], true); - } + Utils::$context['message']['body'] = Parser::transform( + string: Utils::$context['message']['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => (int) $row['id_msg']], + ); Utils::$context['message']['body'] = Utils::adjustHeadingLevels(Utils::$context['message']['body'], null); } diff --git a/Sources/Actions/Memberlist.php b/Sources/Actions/Memberlist.php index eedfc03377..b63ca33738 100644 --- a/Sources/Actions/Memberlist.php +++ b/Sources/Actions/Memberlist.php @@ -17,13 +17,13 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; use SMF\IntegrationHook; use SMF\Lang; use SMF\PageIndex; +use SMF\Parser; use SMF\Theme; use SMF\Time; use SMF\User; @@ -732,7 +732,10 @@ public static function printRows(object $request): void } if ($column['bbc'] && !empty(Utils::$context['members'][$member]['options'][$key])) { - Utils::$context['members'][$member]['options'][$key] = strip_tags(BBCodeParser::load()->parse(Utils::$context['members'][$member]['options'][$key])); + Utils::$context['members'][$member]['options'][$key] = Parser::transform( + string: Utils::$context['members'][$member]['options'][$key], + output_type: Parser::OUTPUT_TEXT, + ); } elseif ($column['type'] == 'check') { Utils::$context['members'][$member]['options'][$key] = Utils::$context['members'][$member]['options'][$key] == 0 ? Lang::$txt['no'] : Lang::$txt['yes']; } diff --git a/Sources/Actions/MessageIndex.php b/Sources/Actions/MessageIndex.php index 4f5288e533..f15345f9e2 100644 --- a/Sources/Actions/MessageIndex.php +++ b/Sources/Actions/MessageIndex.php @@ -17,7 +17,6 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Board; use SMF\Category; use SMF\Config; @@ -25,8 +24,8 @@ use SMF\ErrorHandler; use SMF\IntegrationHook; use SMF\Lang; -use SMF\MarkdownParser; use SMF\PageIndex; +use SMF\Parser; use SMF\Theme; use SMF\Time; use SMF\User; @@ -244,13 +243,14 @@ public static function buildTopicContext(array $row): void // Does the theme support message previews? if (!empty(Config::$modSettings['preview_characters'])) { - // Limit them to Config::$modSettings['preview_characters'] characters - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['first_body'] = MarkdownParser::load(MarkdownParser::OUTPUT_BBC)->parse($row['first_body']); - } - - $row['first_body'] = strip_tags(strtr(BBCodeParser::load()->parse($row['first_body'], (bool) $row['first_smileys'], (int) $row['id_first_msg']), ['
' => ' '])); + $row['first_body'] = Parser::transform( + string: $row['first_body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['first_smileys'] ? Parser::INPUT_SMILEYS : 0), + output_type: Parser::OUTPUT_TEXT, + options: ['str_replace' => ['
' => ' ']], + ); + // Limit them to Config::$modSettings['preview_characters'] characters if (Utils::entityStrlen($row['first_body']) > Config::$modSettings['preview_characters']) { $row['first_body'] = Utils::entitySubstr($row['first_body'], 0, (int) Config::$modSettings['preview_characters']) . '...'; } @@ -264,11 +264,12 @@ public static function buildTopicContext(array $row): void $row['last_subject'] = $row['first_subject']; $row['last_body'] = $row['first_body']; } else { - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['last_body'] = MarkdownParser::load(MarkdownParser::OUTPUT_BBC)->parse($row['last_body']); - } - - $row['last_body'] = strip_tags(strtr(BBCodeParser::load()->parse($row['last_body'], (bool) $row['last_smileys'], (int) $row['id_last_msg']), ['
' => ' '])); + $row['last_body'] = Parser::transform( + string: $row['last_body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['last_smileys'] ? Parser::INPUT_SMILEYS : 0), + output_type: Parser::OUTPUT_TEXT, + options: ['str_replace' => ['
' => ' ']], + ); if (Utils::entityStrlen($row['last_body']) > Config::$modSettings['preview_characters']) { $row['last_body'] = Utils::entitySubstr($row['last_body'], 0, (int) Config::$modSettings['preview_characters']) . '...'; diff --git a/Sources/Actions/Moderation/Home.php b/Sources/Actions/Moderation/Home.php index 291173f498..5714772bc0 100644 --- a/Sources/Actions/Moderation/Home.php +++ b/Sources/Actions/Moderation/Home.php @@ -17,7 +17,6 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Cache\CacheApi; use SMF\Config; use SMF\Db\DatabaseApi as Db; @@ -25,6 +24,7 @@ use SMF\IntegrationHook; use SMF\Lang; use SMF\PageIndex; +use SMF\Parser; use SMF\SecurityToken; use SMF\Theme; use SMF\Time; @@ -295,11 +295,7 @@ protected function notes(): void Utils::$context['notes'] = []; foreach ($moderator_notes as $note) { - $note['body'] = BBCodeParser::load()->parse($note['body']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $note['body'] = MarkdownParser::load()->parse($note['body'], true); - } + $note['body'] = Parser::transform($note['body']); Utils::$context['notes'][] = [ 'author' => [ diff --git a/Sources/Actions/Moderation/Posts.php b/Sources/Actions/Moderation/Posts.php index bfaed2a1d5..951f43297c 100644 --- a/Sources/Actions/Moderation/Posts.php +++ b/Sources/Actions/Moderation/Posts.php @@ -19,7 +19,6 @@ use SMF\Actions\BackwardCompatibility; use SMF\ActionTrait; use SMF\Attachment; -use SMF\BBCodeParser; use SMF\Board; use SMF\Config; use SMF\Db\DatabaseApi as Db; @@ -27,10 +26,10 @@ use SMF\ItemList; use SMF\Lang; use SMF\Logging; -use SMF\MarkdownParser; use SMF\Menu; use SMF\Msg; use SMF\PageIndex; +use SMF\Parser; use SMF\SecurityToken; use SMF\Theme; use SMF\Time; @@ -380,11 +379,11 @@ public function posts(): void $can_delete = false; } - $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], (int) $row['id_msg']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } + $row['body'] = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['last_smileys'] ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => (int) $row['id_msg']], + ); Utils::$context['unapproved_items'][] = [ 'id' => $row['id_msg'], @@ -784,11 +783,7 @@ public static function list_getUnapprovedAttachments(int $start, int $items_per_ ); while ($row = Db::$db->fetch_assoc($request)) { - $row['body'] = BBCodeParser::load()->parse($row['body']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } + $row['body'] = Parser::transform($row['body']); $unapproved_items[] = [ 'id' => $row['id_attach'], diff --git a/Sources/Actions/Moderation/ReportedContent.php b/Sources/Actions/Moderation/ReportedContent.php index 0d847b5ca6..64e80520cb 100644 --- a/Sources/Actions/Moderation/ReportedContent.php +++ b/Sources/Actions/Moderation/ReportedContent.php @@ -19,7 +19,6 @@ use SMF\Actions\BackwardCompatibility; use SMF\ActionTrait; use SMF\Alert; -use SMF\BBCodeParser; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; @@ -28,9 +27,9 @@ use SMF\ItemList; use SMF\Lang; use SMF\Logging; -use SMF\MarkdownParser; use SMF\Menu; use SMF\PageIndex; +use SMF\Parser; use SMF\SecurityToken; use SMF\Theme; use SMF\Time; @@ -253,12 +252,6 @@ public function details(): void ], ]; } else { - $report['body'] = BBCodeParser::load()->parse($report['body']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $report['body'] = MarkdownParser::load()->parse($report['body'], true); - } - $extraDetails = [ 'topic_id' => $report['id_topic'], 'board_id' => $report['id_board'], @@ -272,7 +265,7 @@ public function details(): void 'href' => Config::$scripturl . '?action=profile;u=' . $report['id_author'], ], 'subject' => $report['subject'], - 'body' => $report['body'], + 'body' => Parser::transform($report['body']), ]; } @@ -937,12 +930,6 @@ protected function getReports(int $closed = 0): array } else { $report_boards_ids[] = $row['id_board']; - $row['body'] = BBCodeParser::load()->parse($row['body']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } - $extraDetails = [ 'topic' => [ 'id' => $row['id_topic'], @@ -957,7 +944,7 @@ protected function getReports(int $closed = 0): array 'href' => Config::$scripturl . '?action=profile;u=' . $row['id_author'], ], 'subject' => $row['subject'], - 'body' => $row['body'], + 'body' => Parser::transform($row['body']), ]; } @@ -1149,15 +1136,9 @@ protected function getReportComments(int $report_id): array ); while ($row = Db::$db->fetch_assoc($request)) { - $row['body'] = BBCodeParser::load()->parse($row['body']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } - $report['mod_comments'][] = [ 'id' => $row['id_comment'], - 'message' => $row['body'], + 'message' => Parser::transform($row['body']), 'time' => Time::create('@' . $row['log_time'])->format(), 'can_edit' => User::$me->allowedTo('admin_forum') || ((User::$me->id == $row['id_member'])), 'member' => [ diff --git a/Sources/Actions/Moderation/ShowNotice.php b/Sources/Actions/Moderation/ShowNotice.php index a2eaccada0..857be78872 100644 --- a/Sources/Actions/Moderation/ShowNotice.php +++ b/Sources/Actions/Moderation/ShowNotice.php @@ -17,11 +17,10 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; use SMF\Lang; -use SMF\MarkdownParser; +use SMF\Parser; use SMF\Theme; use SMF\User; use SMF\Utils; @@ -60,11 +59,10 @@ public function execute(): void list(Utils::$context['notice_body'], Utils::$context['notice_subject']) = Db::$db->fetch_row($request); Db::$db->free_result($request); - Utils::$context['notice_body'] = BBCodeParser::load()->parse(Utils::$context['notice_body'], false); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - Utils::$context['notice_body'] = MarkdownParser::load()->parse(Utils::$context['notice_body'], true); - } + Utils::$context['notice_body'] = Parser::transform( + string: Utils::$context['notice_body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN, + ); } /****************** diff --git a/Sources/Actions/Moderation/WatchedUsers.php b/Sources/Actions/Moderation/WatchedUsers.php index 0f86933401..733c83667b 100644 --- a/Sources/Actions/Moderation/WatchedUsers.php +++ b/Sources/Actions/Moderation/WatchedUsers.php @@ -17,14 +17,13 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\ItemList; use SMF\Lang; -use SMF\MarkdownParser; use SMF\Menu; use SMF\Msg; +use SMF\Parser; use SMF\Theme; use SMF\Time; use SMF\User; @@ -428,11 +427,11 @@ public static function list_getWatchedUserPosts(int $start, int $items_per_page, $row['subject'] = Lang::censorText($row['subject']); $row['body'] = Lang::censorText($row['body']); - $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], (int) $row['id_msg']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } + $row['body'] = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['last_smileys'] ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => (int) $row['id_msg']], + ); $member_posts[$row['id_msg']] = [ 'id' => $row['id_msg'], diff --git a/Sources/Actions/Post.php b/Sources/Actions/Post.php index 85307c232e..d813585ae0 100644 --- a/Sources/Actions/Post.php +++ b/Sources/Actions/Post.php @@ -18,7 +18,6 @@ use SMF\ActionInterface; use SMF\ActionTrait; use SMF\Attachment; -use SMF\BBCodeParser; use SMF\Board; use SMF\Cache\CacheApi; use SMF\Calendar\Event; @@ -29,8 +28,8 @@ use SMF\ErrorHandler; use SMF\IntegrationHook; use SMF\Lang; -use SMF\MarkdownParser; use SMF\Msg; +use SMF\Parser; use SMF\Poll; use SMF\Security; use SMF\Theme; @@ -454,11 +453,11 @@ protected function getTopicSummary(): void // Censor, BBC, ... Lang::censorText($row['body']); - $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], (int) $row['id_msg']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } + $row['body'] = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => (int) $row['id_msg']], + ); IntegrationHook::call('integrate_getTopic_previous_post', [&$row]); @@ -958,11 +957,10 @@ protected function showPreview(): void Msg::preparsecode(Utils::$context['preview_message']); // Do all bulletin board code tags, with or without smileys. - Utils::$context['preview_message'] = BBCodeParser::load()->parse(Utils::$context['preview_message'], !isset($_REQUEST['ns'])); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - Utils::$context['preview_message'] = MarkdownParser::load()->parse(Utils::$context['preview_message'], true); - } + Utils::$context['preview_message'] = Parser::transform( + string: Utils::$context['preview_message'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | (isset($_REQUEST['ns']) ? Parser::INPUT_SMILEYS : 0), + ); Lang::censorText(Utils::$context['preview_message']); diff --git a/Sources/Actions/Post2.php b/Sources/Actions/Post2.php index 87e92ecba0..ba85bd2119 100644 --- a/Sources/Actions/Post2.php +++ b/Sources/Actions/Post2.php @@ -17,7 +17,6 @@ use SMF\Attachment; use SMF\Autolinker; -use SMF\BBCodeParser; use SMF\Board; use SMF\BrowserDetector; use SMF\Cache\CacheApi; @@ -30,8 +29,8 @@ use SMF\IntegrationHook; use SMF\Lang; use SMF\Logging; -use SMF\MarkdownParser; use SMF\Msg; +use SMF\Parser; use SMF\Poll; use SMF\Search\SearchApi; use SMF\Security; @@ -269,11 +268,10 @@ public function submit(): void Msg::preparsecode($_POST['message']); // Let's see if there's still some content left without the tags. - $temp = BBCodeParser::load()->parse($_POST['message'], false); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $temp = MarkdownParser::load()->parse($temp, true); - } + $temp = Parser::transform( + string: $_POST['message'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN, + ); if (Utils::htmlTrim(strip_tags($temp, implode('', Utils::$context['allowed_html_tags']))) === '' && (!User::$me->allowedTo('bbc_html') || !str_contains($_POST['message'], '[html]'))) { $this->errors[] = 'no_message'; diff --git a/Sources/Actions/Profile/BuddyIgnoreLists.php b/Sources/Actions/Profile/BuddyIgnoreLists.php index 8c8d263ad1..03bfec4a6f 100644 --- a/Sources/Actions/Profile/BuddyIgnoreLists.php +++ b/Sources/Actions/Profile/BuddyIgnoreLists.php @@ -17,13 +17,13 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; use SMF\IntegrationHook; use SMF\Lang; use SMF\Menu; +use SMF\Parser; use SMF\Profile; use SMF\Theme; use SMF\User; @@ -295,7 +295,11 @@ public function buddies(): void } if ($column['bbc'] && !empty(Utils::$context['buddies'][$buddy]['options'][$key])) { - Utils::$context['buddies'][$buddy]['options'][$key] = strip_tags(BBCodeParser::load()->parse(Utils::$context['buddies'][$buddy]['options'][$key])); + Utils::$context['buddies'][$buddy]['options'][$key] = Parser::transform( + string: Utils::$context['buddies'][$buddy]['options'][$key], + output_type: Parser::OUTPUT_TEXT, + options: ['hard_breaks' => 0], + ); } elseif ($column['type'] == 'check') { Utils::$context['buddies'][$buddy]['options'][$key] = Utils::$context['buddies'][$buddy]['options'][$key] == 0 ? Lang::$txt['no'] : Lang::$txt['yes']; } diff --git a/Sources/Actions/Profile/IssueWarning.php b/Sources/Actions/Profile/IssueWarning.php index 50ec05c7cc..58fe724f3b 100644 --- a/Sources/Actions/Profile/IssueWarning.php +++ b/Sources/Actions/Profile/IssueWarning.php @@ -17,14 +17,13 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; use SMF\ItemList; use SMF\Lang; -use SMF\MarkdownParser; use SMF\Msg; +use SMF\Parser; use SMF\PersonalMessage\PM; use SMF\Profile; use SMF\Time; @@ -538,11 +537,7 @@ protected function preview(): void if (!empty($_POST['warn_body'])) { Msg::preparsecode($warning_body, false, !empty(Config::$modSettings['autoLinkUrls'])); - $warning_body = BBCodeParser::load()->parse($warning_body); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $warning_body = MarkdownParser::load()->parse($warning_body, true); - } + $warning_body = Parser::transform($warning_body); } // Try to remember some bits. diff --git a/Sources/Actions/Profile/ShowPosts.php b/Sources/Actions/Profile/ShowPosts.php index 1373e40f5c..10dc82a6c4 100644 --- a/Sources/Actions/Profile/ShowPosts.php +++ b/Sources/Actions/Profile/ShowPosts.php @@ -18,7 +18,6 @@ use SMF\ActionInterface; use SMF\ActionTrait; use SMF\Autolinker; -use SMF\BBCodeParser; use SMF\Board; use SMF\Config; use SMF\Db\DatabaseApi as Db; @@ -27,10 +26,10 @@ use SMF\ItemList; use SMF\Lang; use SMF\Logging; -use SMF\MarkdownParser; use SMF\Menu; use SMF\Msg; use SMF\PageIndex; +use SMF\Parser; use SMF\Profile; use SMF\Theme; use SMF\Time; @@ -839,11 +838,11 @@ protected function loadPosts(bool $is_topics = false): void } // Do the code. - $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], (int) $row['id_msg']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } + $row['body'] = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => (int) $row['id_msg']], + ); // And the array... Utils::$context['posts'][$counter += $reverse ? -1 : 1] = [ diff --git a/Sources/Actions/Profile/Tracking.php b/Sources/Actions/Profile/Tracking.php index ae0854d4df..011557dea9 100644 --- a/Sources/Actions/Profile/Tracking.php +++ b/Sources/Actions/Profile/Tracking.php @@ -18,7 +18,6 @@ use SMF\ActionInterface; use SMF\Actions\TrackIP; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; @@ -26,6 +25,7 @@ use SMF\ItemList; use SMF\Lang; use SMF\Menu; +use SMF\Parser; use SMF\Profile; use SMF\Time; use SMF\User; @@ -745,8 +745,8 @@ public static function list_getProfileEdits(int $start, int $items_per_page, str 'member_link' => Lang::$txt['trackEdit_deleted_member'], 'action' => $row['action'], 'action_text' => $action_text, - 'before' => !empty($extra['previous']) ? ($parse_bbc ? Utils::adjustHeadingLevels(BBCodeParser::load()->parse($extra['previous']), null) : $extra['previous']) : '', - 'after' => !empty($extra['new']) ? ($parse_bbc ? Utils::adjustHeadingLevels(BBCodeParser::load()->parse($extra['new']), null) : $extra['new']) : '', + 'before' => !empty($extra['previous']) ? ($parse_bbc ? Utils::adjustHeadingLevels(Parser::transform($extra['previous']), null) : $extra['previous']) : '', + 'after' => !empty($extra['new']) ? ($parse_bbc ? Utils::adjustHeadingLevels(Parser::transform($extra['new']), null) : $extra['new']) : '', 'time' => Time::create('@' . $row['log_time'])->format(), ]; } diff --git a/Sources/Actions/Recent.php b/Sources/Actions/Recent.php index 2238a0248e..9b947a437b 100644 --- a/Sources/Actions/Recent.php +++ b/Sources/Actions/Recent.php @@ -17,7 +17,6 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Board; use SMF\Cache\CacheApi; use SMF\Config; @@ -25,9 +24,9 @@ use SMF\ErrorHandler; use SMF\IntegrationHook; use SMF\Lang; -use SMF\MarkdownParser; use SMF\Msg; use SMF\PageIndex; +use SMF\Parser; use SMF\Theme; use SMF\Time; use SMF\User; @@ -175,11 +174,11 @@ public static function getLastPost(): array Lang::censorText($row['subject']); Lang::censorText($row['body']); - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load(MarkdownParser::OUTPUT_BBC)->parse($row['body']); - } - - $row['body'] = strip_tags(strtr(BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled']), ['
' => ' '])); + $row['body'] = Parser::transform( + string: $row['body'], + output_type: Parser::OUTPUT_TEXT, + options: ['str_replace' => ['
' => ' ']], + ); if (Utils::entityStrlen($row['body']) > 128) { $row['body'] = Utils::entitySubstr($row['body'], 0, 128) . '...'; diff --git a/Sources/Actions/Register.php b/Sources/Actions/Register.php index 257576169b..ffec4c6af0 100644 --- a/Sources/Actions/Register.php +++ b/Sources/Actions/Register.php @@ -17,11 +17,10 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Config; use SMF\ErrorHandler; use SMF\Lang; -use SMF\MarkdownParser; +use SMF\Parser; use SMF\Profile; use SMF\SecurityToken; use SMF\Theme; @@ -184,9 +183,21 @@ public function show(): void if (!empty(Config::$modSettings['requireAgreement'])) { // Have we got a localized one? if (file_exists(Config::$languagesdir . '/' . User::$me->language . '/agreement.txt')) { - Utils::$context['agreement'] = BBCodeParser::load()->parse(file_get_contents(Config::$languagesdir . '/' . User::$me->language . '/agreement.txt'), true, 'agreement_' . User::$me->language); + Utils::$context['privacy_policy'] = Parser::transform( + string: file_get_contents(Config::$languagesdir . '/' . User::$me->language . '/agreement.txt'), + options: [ + 'cache_id' => 'agreement_' . User::$me->language, + 'hard_breaks' => 0, + ], + ); } elseif (file_exists(Config::$languagesdir . '/en_US/agreement.txt')) { - Utils::$context['agreement'] = BBCodeParser::load()->parse(file_get_contents(Config::$languagesdir . '/en_US/agreement.txt'), true, 'agreement'); + Utils::$context['privacy_policy'] = Parser::transform( + string: file_get_contents(Config::$languagesdir . '/en_US/agreement.txt'), + options: [ + 'cache_id' => 'agreement', + 'hard_breaks' => 0, + ], + ); } else { Utils::$context['agreement'] = ''; } @@ -197,10 +208,6 @@ public function show(): void ErrorHandler::log(Lang::$txt['registration_agreement_missing'], 'critical'); ErrorHandler::fatalLang('registration_disabled', false); } - - if (!empty(Config::$modSettings['enableMarkdown'])) { - Utils::$context['agreement'] = MarkdownParser::load(MarkdownParser::OUTPUT_HTML, 0)->parse(Utils::$context['agreement'], true); - } } $prefs = Notify::getNotifyPrefs(0, 'announcements'); @@ -229,18 +236,20 @@ public function show(): void if (!empty(Config::$modSettings['requirePolicyAgreement'])) { // Have we got a localized one? if (!empty(Config::$modSettings['policy_' . User::$me->language])) { - Utils::$context['privacy_policy'] = BBCodeParser::load()->parse(Config::$modSettings['policy_' . User::$me->language]); + Utils::$context['privacy_policy'] = Parser::transform( + string: Config::$modSettings['policy_' . User::$me->language], + options: ['hard_breaks' => 0], + ); } elseif (!empty(Config::$modSettings['policy_' . Lang::$default])) { - Utils::$context['privacy_policy'] = BBCodeParser::load()->parse(Config::$modSettings['policy_' . Lang::$default]); + Utils::$context['privacy_policy'] = Parser::transform( + string: Config::$modSettings['policy_' . Lang::$default], + options: ['hard_breaks' => 0], + ); } else { // None was found; log the error so the admin knows there is a problem! ErrorHandler::log(Lang::$txt['registration_policy_missing'], 'critical'); ErrorHandler::fatalLang('registration_disabled', false); } - - if (!empty(Config::$modSettings['enableMarkdown'])) { - Utils::$context['privacy_policy'] = MarkdownParser::load(MarkdownParser::OUTPUT_HTML, 0)->parse(Utils::$context['privacy_policy'], true); - } } // Any custom fields we want filled in? diff --git a/Sources/Actions/TopicPrint.php b/Sources/Actions/TopicPrint.php index 91a67e40e4..49e7afe35e 100644 --- a/Sources/Actions/TopicPrint.php +++ b/Sources/Actions/TopicPrint.php @@ -18,13 +18,12 @@ use SMF\ActionInterface; use SMF\ActionTrait; use SMF\Attachment; -use SMF\BBCodeParser; use SMF\Board; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; use SMF\Lang; -use SMF\MarkdownParser; +use SMF\Parser; use SMF\Poll; use SMF\Theme; use SMF\Time; @@ -97,18 +96,7 @@ public function execute(): void Utils::$context['poll'] = $poll->format(['no_buttons' => true]); } - // We want a separate BBCodeParser instance for this, not the reusable one - // that would be returned by BBCodeParser::load(). - $bbcparser = new BBCodeParser(); - - // Set the BBCodeParser to print mode. - $bbcparser->for_print = true; - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $markdown_parser = new MarkdownParser(); - } - - // Lets "output" all that info. + // Let's output all that info. Theme::loadTemplate('Printpage'); Utils::$context['template_layers'] = ['print']; Utils::$context['board_name'] = Board::$info->name; @@ -143,11 +131,7 @@ public function execute(): void Lang::censorText($row['subject']); Lang::censorText($row['body']); - $row['body'] = $bbcparser->parse($row['body']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = $markdown_parser->parse($row['body'], true); - } + $row['body'] = Parser::transform($row['body'], options: ['for_print' => true]); Utils::$context['posts'][] = [ 'subject' => $row['subject'], diff --git a/Sources/Actions/TopicSplit.php b/Sources/Actions/TopicSplit.php index e0a8ea1850..c055e660ea 100644 --- a/Sources/Actions/TopicSplit.php +++ b/Sources/Actions/TopicSplit.php @@ -20,7 +20,6 @@ use SMF\ActionInterface; use SMF\ActionTrait; use SMF\Autolinker; -use SMF\BBCodeParser; use SMF\Board; use SMF\Config; use SMF\Db\DatabaseApi as Db; @@ -29,9 +28,9 @@ use SMF\Lang; use SMF\Logging; use SMF\Mail; -use SMF\MarkdownParser; use SMF\Msg; use SMF\PageIndex; +use SMF\Parser; use SMF\Search\SearchApi; use SMF\Theme; use SMF\Time; @@ -459,11 +458,11 @@ public function select(): void $row['body'] = Autolinker::load(true)->makeLinks($row['body']); } - $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], (int) $row['id_msg']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } + $row['body'] = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => (int) $row['id_msg']], + ); Utils::$context['not_selected']['messages'][$row['id_msg']] = [ 'id' => $row['id_msg'], @@ -508,11 +507,11 @@ public function select(): void $row['body'] = Autolinker::load(true)->makeLinks($row['body']); } - $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], (int) $row['id_msg']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } + $row['body'] = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => (int) $row['id_msg']], + ); Utils::$context['selected']['messages'][$row['id_msg']] = [ 'id' => $row['id_msg'], diff --git a/Sources/Actions/XmlHttp.php b/Sources/Actions/XmlHttp.php index 7c7ea2cc87..2a7a4f445e 100644 --- a/Sources/Actions/XmlHttp.php +++ b/Sources/Actions/XmlHttp.php @@ -18,7 +18,6 @@ use SMF\ActionInterface; use SMF\Actions\Admin\News; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Board; use SMF\Config; use SMF\Db\DatabaseApi as Db; @@ -26,8 +25,8 @@ use SMF\ErrorHandler; use SMF\IntegrationHook; use SMF\Lang; -use SMF\MarkdownParser; use SMF\Msg; +use SMF\Parser; use SMF\Profile; use SMF\Theme; use SMF\User; @@ -168,7 +167,7 @@ public function newspreview(): void 'identifier' => 'parsedNews', 'children' => [ [ - 'value' => Utils::adjustHeadingLevels(BBCodeParser::load()->parse($news), null), + 'value' => Utils::adjustHeadingLevels(Parser::transform($news), null), ], ], ], @@ -238,16 +237,22 @@ public function sig_preview(): void Lang::censorText($current_signature); - $allowedTags = BBCodeParser::getSigTags(); + $allowedTags = Parser::getSigTags(); - $current_signature = !empty($current_signature) ? BBCodeParser::load()->parse($current_signature, true, 'sig' . $user, $allowedTags) : Lang::$txt['no_signature_set']; + if (empty($current_signature)) { + $current_signature = Lang::$txt['no_signature_set']; + } else { + $current_signature = Parser::transform( + string: $current_signature, + options: [ + 'cache_id' => 'sig' . $user, + 'parse_tags' => $allowedTags, + ], + ); - if (!empty(Config::$modSettings['enableMarkdown'])) { - $current_signature = MarkdownParser::load()->parse($current_signature, true); + $current_signature = Utils::adjustHeadingLevels($current_signature, null); } - $current_signature = Utils::adjustHeadingLevels($current_signature, null); - $preview_signature = !empty($_POST['signature']) ? Utils::htmlspecialchars($_POST['signature']) : Lang::$txt['no_signature_preview']; $validation = Profile::validateSignature($preview_signature); @@ -258,11 +263,13 @@ public function sig_preview(): void Lang::censorText($preview_signature); - $preview_signature = BBCodeParser::load()->parse($preview_signature, true, 'sig' . $user, $allowedTags); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $preview_signature = MarkdownParser::load()->parse($preview_signature, true); - } + $preview_signature = Parser::transform( + string: $preview_signature, + options: [ + 'cache_id' => 'sig' . $user, + 'parse_tags' => $allowedTags, + ], + ); $preview_signature = Utils::adjustHeadingLevels($preview_signature, null); } elseif (!$can_change) { @@ -367,12 +374,7 @@ public function warning_preview(): void if (!empty($_POST['body'])) { Msg::preparsecode($warning_body, false, !empty(Config::$modSettings['autoLinkUrls'])); - $warning_body = BBCodeParser::load()->parse($warning_body); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $warning_body = MarkdownParser::load()->parse($warning_body, true); - } - + $warning_body = Parser::transform($warning_body); $warning_body = Utils::adjustHeadingLevels($warning_body, null); } diff --git a/Sources/Autolinker.php b/Sources/Autolinker.php index b0fef90c79..66b9becdcb 100644 --- a/Sources/Autolinker.php +++ b/Sources/Autolinker.php @@ -117,7 +117,7 @@ class Autolinker * BBCodes whose content should be skipped when autolinking URLs. * * Mods can add to this list using the integrate_bbc_codes hook in - * BBCodeParser::integrateBBC() + * Parsers\BBCodeParser::integrateBBC() */ public static array $no_autolink_tags = [ 'url', @@ -280,8 +280,8 @@ public function __construct(bool $only_basic = false) // For historical reasons, the integrate_bbc_codes hook is used to give // mods access to Autolinker::$no_autolink_tags. The easiest way to - // trigger a call to that hook is to call BBCodeParser::getCodes(). - BBCodeParser::getCodes(); + // trigger a call to that hook is to call Parser::getBBCodes(). + Parser::getBBCodes(); } /** diff --git a/Sources/Board.php b/Sources/Board.php index ff38ab9674..717b46ec11 100644 --- a/Sources/Board.php +++ b/Sources/Board.php @@ -879,14 +879,22 @@ public function parseDescription(): void } if (!isset(self::$parsed_descriptions[$this->id])) { - self::$parsed_descriptions[$this->id] = BBCodeParser::load()->parse($this->description, false, '', Utils::$context['description_allowed_tags']); + self::$parsed_descriptions[$this->id] = Parser::transform( + string: self::$parsed_descriptions[$this->id], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN, + options: ['parse_tags' => Utils::$context['description_allowed_tags']], + ); CacheApi::put('parsed_boards_descriptions', self::$parsed_descriptions, 864000); } $this->description = self::$parsed_descriptions[$this->id]; } else { - $this->description = BBCodeParser::load()->parse($this->description, false, '', Utils::$context['description_allowed_tags']); + $this->description = Parser::transform( + string: $this->description, + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN, + options: ['parse_tags' => Utils::$context['description_allowed_tags']], + ); } } diff --git a/Sources/Category.php b/Sources/Category.php index 53b910d967..c5f2272abc 100644 --- a/Sources/Category.php +++ b/Sources/Category.php @@ -227,14 +227,22 @@ public function parseDescription(): void } if (!isset(self::$parsed_descriptions[$this->id])) { - self::$parsed_descriptions[$this->id] = BBCodeParser::load()->parse($this->description, false, '', Utils::$context['description_allowed_tags']); + self::$parsed_descriptions[$this->id] = Parser::transform( + string: self::$parsed_descriptions[$this->id], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN, + options: ['parse_tags' => Utils::$context['description_allowed_tags']], + ); CacheApi::put('parsed_category_descriptions', self::$parsed_descriptions, 864000); } $this->description = self::$parsed_descriptions[$this->id]; } else { - $this->description = BBCodeParser::load()->parse($this->description, false, '', Utils::$context['description_allowed_tags']); + $this->description = Parser::transform( + string: $this->description, + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN, + options: ['parse_tags' => Utils::$context['description_allowed_tags']], + ); } } diff --git a/Sources/Draft.php b/Sources/Draft.php index 1b4f4d3a0d..e6c3a3784a 100644 --- a/Sources/Draft.php +++ b/Sources/Draft.php @@ -505,11 +505,11 @@ public static function showInProfile(int $memID): void Lang::censorText($row['subject']); // BBC-ilize the message. - $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], 'draft' . $row['id_draft']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } + $row['body'] = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => 'draft' . $row['id_draft']], + ); // And the array... Utils::$context['drafts'][$counter += $reverse ? -1 : 1] = [ diff --git a/Sources/Mentions.php b/Sources/Mentions.php index c91effd860..afe554b31d 100644 --- a/Sources/Mentions.php +++ b/Sources/Mentions.php @@ -1,4 +1,5 @@ formatted['body'] = Autolinker::load(true)->makeLinks($this->formatted['body']); } - // Run BBC interpreter on the message. - $this->formatted['body'] = BBCodeParser::load()->parse($this->formatted['body'], $this->smileys_enabled, $this->id); - - // Run the Markdown interpreter on the message. - if (!empty(Config::$modSettings['enableMarkdown'])) { - $this->formatted['body'] = MarkdownParser::load()->parse($this->formatted['body'], true); - } + // Run BBC and Markdown interpreters on the message. + $this->formatted['body'] = Parser::transform( + string: $this->formatted['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ($this->smileys_enabled ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => $this->id], + ); $this->formatted['link'] = '' . $this->formatted['subject'] . ''; @@ -588,7 +587,7 @@ public static function get(array|int $ids, array $query_customizations = []): \G // passed to the queryData() method. IntegrationHook::call('integrate_query_message', [&$selects, &$joins, &$params, &$where, &$order, &$group, &$limit]); - foreach(self::queryData($selects, $params, $joins, $where, $order, $group, $limit) as $row) { + foreach (self::queryData($selects, $params, $joins, $where, $order, $group, $limit) as $row) { $id = (int) $row['id_msg']; yield (new self($id, $row)); @@ -877,7 +876,7 @@ function ($m) { $tags = []; - foreach (BBCodeParser::getCodes() as $code) { + foreach (Parser::getBBCodes() as $code) { if (!in_array($code['tag'], $allowed_empty)) { $tags[] = $code['tag']; } diff --git a/Sources/PackageManager/PackageManager.php b/Sources/PackageManager/PackageManager.php index 5c14b03c36..a7eb788829 100644 --- a/Sources/PackageManager/PackageManager.php +++ b/Sources/PackageManager/PackageManager.php @@ -13,7 +13,6 @@ namespace SMF\PackageManager; -use SMF\BBCodeParser; use SMF\Cache\CacheApi; use SMF\Config; use SMF\Db\DatabaseApi as Db; @@ -24,6 +23,7 @@ use SMF\Logging; use SMF\Menu; use SMF\Msg; +use SMF\Parser; use SMF\Sapi; use SMF\Security; use SMF\Theme; @@ -434,7 +434,7 @@ public function installTest(): void if (!empty($action['parse_bbc'])) { Utils::$context[$type] = preg_replace('~\[[/]?html\]~i', '', Utils::$context[$type]); Msg::preparsecode(Utils::$context[$type]); - Utils::$context[$type] = BBCodeParser::load()->parse(Utils::$context[$type]); + Utils::$context[$type] = Parser::transform(Utils::$context[$type]); } else { Utils::$context[$type] = nl2br(Utils::$context[$type]); } @@ -1188,7 +1188,7 @@ public function install(): void if (!empty($action['parse_bbc'])) { Utils::$context['redirect_text'] = preg_replace('~\[[/]?html\]~i', '', Utils::$context['redirect_text']); Msg::preparsecode(Utils::$context['redirect_text']); - Utils::$context['redirect_text'] = BBCodeParser::load()->parse(Utils::$context['redirect_text']); + Utils::$context['redirect_text'] = Parser::transform(Utils::$context['redirect_text']); } // Parse out a couple of common urls. @@ -1536,7 +1536,7 @@ public function examineFile(): void } if (strtolower(strrchr($_REQUEST['file'], '.')) == '.php') { - Utils::$context['filedata'] = BBCodeParser::highlightPhpCode(Utils::$context['filedata']); + Utils::$context['filedata'] = Parser::highlightPhpCode(Utils::$context['filedata']); } } } @@ -1869,8 +1869,8 @@ public function showOperations(): void // Let's do some formatting... $operation_text = Utils::$context['operations']['position'] == 'replace' ? 'operation_replace' : (Utils::$context['operations']['position'] == 'before' ? 'operation_after' : 'operation_before'); - Utils::$context['operations']['search'] = BBCodeParser::load()->parse('[code=' . Lang::$txt['operation_find'] . ']' . (Utils::$context['operations']['position'] == 'end' ? '?>' : Utils::$context['operations']['search']) . '[/code]'); - Utils::$context['operations']['replace'] = BBCodeParser::load()->parse('[code=' . Lang::$txt[$operation_text] . ']' . Utils::$context['operations']['replace'] . '[/code]'); + Utils::$context['operations']['search'] = Parser::transform('[code=' . Lang::$txt['operation_find'] . ']' . (Utils::$context['operations']['position'] == 'end' ? '?>' : Utils::$context['operations']['search']) . '[/code]'); + Utils::$context['operations']['replace'] = Parser::transform('[code=' . Lang::$txt[$operation_text] . ']' . Utils::$context['operations']['replace'] . '[/code]'); // No layers Utils::$context['template_layers'] = []; @@ -2776,7 +2776,7 @@ public function serverBrowse(): void if ($package['description'] == '') { $package['description'] = Lang::$txt['package_no_description']; } else { - $package['description'] = Utils::adjustHeadingLevels(BBCodeParser::load()->parse(preg_replace('~\[[/]?html\]~i', '', Utils::htmlspecialchars($package['description']))), null); + $package['description'] = Utils::adjustHeadingLevels(Parser::transform(preg_replace('~\[[/]?html\]~i', '', Utils::htmlspecialchars($package['description']))), null); } $package['is_installed'] = isset($installed_mods[$package['id']]); diff --git a/Sources/Parser.php b/Sources/Parser.php new file mode 100644 index 0000000000..930a3293b8 --- /dev/null +++ b/Sources/Parser.php @@ -0,0 +1,623 @@ + tags pointing to smiley images. + * + * When the output type is plain text, this controls whether tags for + * smiley images will be transformed into smiley text or removed. + * + * When the output type is BBCode, this controls whether tags for + * smiley images will be transformed into smiley text or [img] BBCodes. + */ + public const INPUT_SMILEYS = 0b100; + + /** + * @var int + * + * Used to set the output to HTML. + * + * This is the default output type. + */ + public const OUTPUT_HTML = 0; + + /** + * @var int + * + * Used to set the output to plain text. + * + * When this is used, the input will be parsed into HTML and then the HTML + * tags will be stripped. + */ + public const OUTPUT_TEXT = 1; + + /** + * @var int + * + * Used to set the output to BBCode. + * + * When this is used, HTML and Markdown in the input will be transformed + * into the equivalent BBCode. Unsupported HTML tags will be removed. + */ + public const OUTPUT_BBC = 2; + + /************************** + * Public static properties + **************************/ + + /** + * @var array + * + * Default options for the various parsers. + * + * - cache_id: + * ID string to identify the string for caching purposes. + * If empty, an ID will be generated automatically. + * Default: '' + * + * - parse_tags: + * A list of specific BBC tags to parse. If empty, all BBC are parsed. + * Default: [] + * + * - for_print: + * Whether the output is intended for a non-interactive medium, such + * as being printed on paper. + * Default: false + * + * - hard_breaks: + * Controls how line breaks are handled by MarkdownParser. For more + * info, see the documentation for MarkdownParser::__construct(). + * Default: null + * + * - str_replace: + * String replacements to apply when converting to plain text. + * Keys are the strings to find, and values are the replacements. + * These replacements are applied after the input has been transformed + * into HTML and before the HTML tags are stripped out. + * Default: [] + * + * - preg_replace: + * Similar to str_replace, except that the keys are regular expressions. + * Default: [] + * + * Mods implementing custom parsers can add values to this array using the + * integrate_parser_options hook. + */ + public static array $defalt_options = [ + 'cache_id' => '', + 'parse_tags' => [], + 'for_print' => false, + 'hard_breaks' => null, + 'str_replace' => [], + 'preg_replace' => [], + ]; + + /** + * @var bool + * + * Whether BBCode should be parsed. + */ + public static bool $enable_bbc; + + /** + * @var bool + * + * Whether to allow certain basic HTML tags in the input. + */ + public static bool $enable_post_html; + + /** + * @var bool + * + * Whether Markdown should be parsed. + */ + public static bool $enable_markdown; + + /** + * @var string + * + * The smiley set to use when parsing smileys. + */ + public static string $smiley_set; + + /** + * @var bool + * + * Whether custom smileys are enabled. + */ + public static bool $custom_smileys_enabled; + + /** + * @var string + * + * URL of the base smileys directory. + */ + public static string $smileys_url; + + /** + * @var string + * + * The character encoding of the strings to be parsed. + */ + public static string $encoding; + + /** + * @var string + * + * Language locale to use. + */ + public static string $locale; + + /** + * @var int + * + * User's time offset from UTC. + */ + public static int $time_offset; + + /** + * @var string + * + * User's time format string. + */ + public static string $time_format; + + /**************************** + * Internal static properties + ****************************/ + + /** + * @var array + * + * Holds parsed messages. + */ + private static array $results = []; + + /***************** + * Public methods. + *****************/ + + /** + * Constructor. + */ + public function __construct() + { + self::setStaticVars(); + } + + /*********************** + * Public static methods + ***********************/ + + /** + * Transforms one type of markup into another. + * + * Supported input markup types are BBCode, Markdown, and smileys. + * Supported output markup types are HTML, BBCode, and plain text. + * + * @param string $string The string in which to transform markup. + * @param int $input_types Bitmask of this class's INPUT_* constants. + * Only the indicated types of markup will be parsed in the input string. + * Default: self::INPUT_BBC | self::INPUT_MARKDOWN | self::INPUT_SMILEYS + * @param int $output_type One of this class's INPUT_* constants. + * Default: self::OUTPUT_HTML + * @param array $options Various parser options. See self::$default_options. + * @return string The transformed string. + */ + public static function transform( + string $string, + int $input_types = self::INPUT_BBC | self::INPUT_MARKDOWN | self::INPUT_SMILEYS, + int $output_type = self::OUTPUT_HTML, + array $options = [], + ): string { + self::setStaticVars(); + + // Fill in any missing options. + $options = self::setOptions($options); + + // Map output types to handlers. + $handlers = [ + self::OUTPUT_HTML => __CLASS__ . '::toHTML', + self::OUTPUT_TEXT => __CLASS__ . '::toText', + self::OUTPUT_BBC => __CLASS__ . '::toBBC', + ]; + + // Allow mods to add their own handlers. + IntegrationHook::call('integrate_parser_output_handlers', [&$handlers]); + + // If BBCode or Markdown are disabled, respect that. + if (!self::$enable_bbc /* && !self::$enable_post_html */) { + $input_types = $input_types & ~self::INPUT_BBC; + } + + if (!self::$enable_markdown) { + $input_types = $input_types & ~self::INPUT_MARKDOWN; + } + + // Do nothing if the requested output type is invalid. + if (!is_callable($handlers[$output_type] ?? null)) { + return $string; + } + + // Have we already parsed this string? + // Or maybe we cached the results recently? + $cache_key = self::getCacheKey($string, $input_types, $output_type, $options); + + if ((self::$results[$cache_key] = CacheApi::get($cache_key, 240)) != null) { + return self::$results[$cache_key]; + } + + // Keep track of how long this takes. + $cache_t = microtime(true); + + // Do the job. + self::$results[$cache_key] = $handlers[$output_type]($string, $input_types, $options); + + // Cache the output if it took some time... + if (!empty(CacheApi::$enable) && microtime(true) - $cache_t > pow(50, -CacheApi::$enable)) { + CacheApi::put($cache_key, self::$results[$cache_key], 240); + } + + return self::$results[$cache_key]; + } + + /** + * Get the list of supported BBCodes, including any added by modifications. + * + * @return array List of supported BBCodes. + */ + public static function getBBCodes(): array + { + return BBcodeParser::getCodes(); + } + + /** + * Returns an array of BBCodes tags that are allowed in signatures. + * + * @return array An array containing allowed tags for signatures, or an + * empty array if all tags are allowed. + */ + public static function getSigTags(): array + { + return BBcodeParser::getSigTags(); + } + + /** + * Highlight any code. + * + * Uses PHP's highlight_string() to highlight PHP syntax. + * Does special handling to keep the tabs in the code available. + * Used to parse PHP code from inside [code] and [php] tags. + * + * @param string $code The code. + * @return string The code with highlighted HTML. + */ + public static function highlightPhpCode(string $code): string + { + // Remove special characters. + $code = Utils::htmlspecialcharsDecode(strtr($code, ['
' => "\n", '
' => "\n", "\t" => Utils::TAB_SUBSTITUTE, '[' => '['])); + + $oldlevel = error_reporting(0); + + $buffer = str_replace(["\n", "\r"], '', @highlight_string($code, true)); + + error_reporting($oldlevel); + + $buffer = preg_replace_callback_array( + [ + '~(?:' . Utils::TAB_SUBSTITUTE . ')+~u' => fn ($matches) => '' . strtr($matches[0], [Utils::TAB_SUBSTITUTE => "\t"]) . '', + '~(\h*)~' => fn ($matches) => $matches[1], + ], + $buffer, + ); + + // PHP 8.3 changed the returned HTML. + $buffer = preg_replace('/^(
)?]*>|<\/code>(<\/pre>)?$/', '', $buffer);
+
+		return strtr($buffer, ['\'' => ''']);
+	}
+
+	/**
+	 * Microsoft uses their own character set Code Page 1252 (CP1252), which is
+	 * a superset of ISO 8859-1, defining several characters between DEC 128 and
+	 * 159 that are not normally displayable. This converts the popular ones
+	 * that appear from a cut and paste from Windows.
+	 *
+	 * @todo In a Unicode-aware world, we probably should not do this any more.
+	 *
+	 * @param string $string The string.
+	 * @return string The sanitized string.
+	 */
+	public static function sanitizeMSCutPaste(string $string): string
+	{
+		if (empty($string)) {
+			return $string;
+		}
+
+		self::setStaticVars();
+
+		// UTF-8 occurrences of MS special characters.
+		$findchars_utf8 = [
+			"\xe2\x80\x9a",	// single low-9 quotation mark, U+201A
+			"\xe2\x80\x9e",	// double low-9 quotation mark, U+201E
+			"\xe2\x80\xa6",	// horizontal ellipsis, U+2026
+			"\xe2\x80\x98",	// left single curly quote, U+2018
+			"\xe2\x80\x99",	// right single curly quote, U+2019
+			"\xe2\x80\x9c",	// left double curly quote, U+201C
+			"\xe2\x80\x9d",	// right double curly quote, U+201D
+		];
+
+		// windows 1252 / iso equivalents
+		$findchars_iso = [
+			chr(130),
+			chr(132),
+			chr(133),
+			chr(145),
+			chr(146),
+			chr(147),
+			chr(148),
+		];
+
+		// safe replacements
+		$replacechars = [
+			',',	// ‚
+			',,',	// „
+			'...',	// …
+			"'",	// ‘
+			"'",	// ’
+			'"',	// “
+			'"',	// ”
+		];
+
+		$string = str_replace(self::$encoding === 'UTF-8' ? $findchars_utf8 : $findchars_iso, $replacechars, $string);
+
+		return $string;
+	}
+
+	/*************************
+	 * Internal static methods
+	 *************************/
+
+	/**
+	 * Sets the values of this class's static variables.
+	 *
+	 * If a variable already has a value, the existing value is not changed.
+	 * This ensures that custom values set by external code are respected.
+	 */
+	protected static function setStaticVars(): void
+	{
+		// Is anything disabled?
+		self::$enable_bbc = self::$enable_bbc ?? !empty(Config::$modSettings['enableBBC']);
+		self::$enable_post_html = self::$enable_post_html ?? !empty(Config::$modSettings['enablePostHTML']);
+		self::$enable_markdown = self::$enable_markdown ?? !empty(Config::$modSettings['enableMarkdown']);
+		self::$custom_smileys_enabled = self::$custom_smileys_enabled ?? !empty(Config::$modSettings['smiley_enable']);
+
+		// Set up localization.
+		if (!isset(User::$me)) {
+			User::setMe(0);
+		}
+
+		self::$time_offset = self::$time_offset ?? User::$me->time_offset ?? 0;
+		self::$time_format = self::$time_format ?? User::$me->time_format ?? Time::getTimeFormat();
+
+		self::$locale = self::$locale ?? Lang::$txt['lang_locale'] ?? '';
+		self::$encoding = self::$encoding ?? (!empty(Utils::$context['utf8']) ? 'UTF-8' : (!empty(Config::$modSettings['global_character_set']) ? Config::$modSettings['global_character_set'] : (!empty(Lang::$txt['lang_character_set']) ? Lang::$txt['lang_character_set'] : 'UTF-8')));
+
+		// Smiley settings.
+		self::$custom_smileys_enabled = self::$custom_smileys_enabled ?? !empty(Config::$modSettings['smiley_enable']);
+		self::$smileys_url = self::$smileys_url ?? Config::$modSettings['smileys_url'];
+		self::$smiley_set = self::$smiley_set ?? (!empty(User::$me->smiley_set) ? User::$me->smiley_set : (!empty(Config::$modSettings['smiley_sets_default']) ? Config::$modSettings['smiley_sets_default'] : 'none'));
+	}
+
+	/**
+	 * Fills in any missing elements of $options with the default values.
+	 *
+	 * @param array $options An array of parser options.
+	 * @return array An updated copy of $options.
+	 */
+	protected static function setOptions(array $options): array
+	{
+		IntegrationHook::call('integrate_parser_options', [&$options]);
+
+		return array_merge(self::$defalt_options, $options);
+	}
+
+	/**
+	 * Transforms the input string into HTML.
+	 *
+	 * @param string $string The string in which to transform markup.
+	 * @param int $input_types Bitmask of this class's INPUT_* constants.
+	 *    Only the indicated types of markup will be parsed in the input string.
+	 * @param array $options An array of parser options.
+	 * @return string The transformed string.
+	 */
+	protected static function toHTML(string $string, int $input_types, array $options): string
+	{
+		// Allow mods access before parsing.
+		$smileys = !empty($input_types & self::INPUT_SMILEYS);
+
+		IntegrationHook::call('integrate_pre_parsebbc', [&$string, &$smileys, &$options['cache_id'], &$options['parse_tags']]);
+
+		$input_types = $input_types | ($smileys ? self::INPUT_SMILEYS : 0);
+
+		// Parse the BBCode.
+		if ($input_types & self::INPUT_BBC) {
+			$string = BBcodeParser::load(!empty($options['for_print']))->parse($string, $options['cache_id'], $options['parse_tags']);
+		}
+
+		// Parse the smileys.
+		if ($input_types & self::INPUT_SMILEYS) {
+			$string = SmileyParser::load()->parse($string);
+		}
+
+		// Parse the Markdown.
+		if ($input_types & self::INPUT_MARKDOWN) {
+			$string = MarkdownParser::load(MarkdownParser::OUTPUT_HTML, (int) $options['hard_breaks'])->parse($string, true);
+		}
+
+		// Allow mods access to the parsed value.
+		IntegrationHook::call('integrate_post_parsebbc', [&$string, $smileys, $options['cache_id'], $options['parse_tags']]);
+
+		return $string;
+	}
+
+	/**
+	 * Transforms the input string into plain text (i.e. removes all markup).
+	 *
+	 * @param string $string The string in which to remove markup.
+	 * @param int $input_types Bitmask of this class's INPUT_* constants.
+	 *    Only the indicated types of markup will be parsed in the input string.
+	 * @param array $options An array of parser options.
+	 * @return string The transformed string.
+	 */
+	protected static function toText(string $string, int $input_types, array $options): string
+	{
+		// When transforming Markdown to plain text, the best results are
+		// obtained by transforming it into BBC as an intermediate stage.
+		if ($input_types & self::INPUT_MARKDOWN) {
+			$string = MarkdownParser::load(MarkdownParser::OUTPUT_BBC)->parse($string, false);
+			$input_types &= ~self::INPUT_MARKDOWN;
+		}
+
+		// Transform smiley images into smiley text.
+		if ($input_types & self::INPUT_SMILEYS) {
+			$string = SmileyParser::load()->unparse($string);
+			$input_types &= ~self::INPUT_SMILEYS;
+		}
+
+		// Ironically enough, the next step is to transform the BBC into HTML.
+		$string = self::toHTML($string, $input_types, $options);
+
+		// Do we have any replacements to make?
+		if (!empty($options['preg_replace'])) {
+			$string = preg_replace_callback_array($options['preg_replace'], $string);
+		}
+
+		if (!empty($options['str_replace'])) {
+			$string = strtr($string, $options['str_replace']);
+		}
+
+		// Strip out the HTML tags and return the result.
+		return strip_tags($string);
+	}
+
+	/**
+	 * Transforms the input string into BBCode.
+	 *
+	 * - Markdown is transformed to the equivalent BBCode.
+	 * - HTML img tags for smileys are transformed to smiley text.
+	 * - Other HTML is transformed to the equivalent BBCode where possible.
+	 * - HTML tags that cannot be transformed are removed.
+	 *
+	 * @param string $string The string in which to remove markup.
+	 * @param int $input_types Bitmask of this class's INPUT_* constants.
+	 *    Only the indicated types of markup will be parsed in the input string.
+	 * @param array $options An array of parser options.
+	 * @return string The transformed string.
+	 */
+	protected static function toBBC(string $string, int $input_types, array $options): string
+	{
+		if ($input_types & self::INPUT_MARKDOWN) {
+			$string = MarkdownParser::load(self::OUTPUT_BBC)->parse($string, false);
+		}
+
+		if ($input_types & self::INPUT_SMILEYS) {
+			$string = SmileyParser::load()->unparse($string);
+		}
+
+		$string = BBcodeParser::load()->unparse($string);
+
+		return $string;
+	}
+
+	/**
+	 * Generates a unique cache key for the combination of string, parameters,
+	 * settings, etc., that apply to this particular call to self::transform().
+	 *
+	 * @param string $string The string in which to transform markup.
+	 * @param int $input_types Bitmask of this class's INPUT_* constants.
+	 * @param int $output_type One of this class's INPUT_* constants.
+	 * @param array $options An array of parser options.
+	 * @return string A unique cache key.
+	 */
+	protected static function getCacheKey(string $string, int $input_types, int $output_type, array $options): string
+	{
+		// Allow mods to add stuff to $cache_key_extras.
+		$cache_key_extras = [];
+
+		IntegrationHook::call('integrate_parser_cache', [&$cache_key_extras, $input_types, $output_type, $options]);
+
+		// If no cache id was given, make a generic one.
+		$cache_id = strval($options['cache_id'] ?? '') !== '' ? $options['cache_id'] : 'str' . substr(md5($string), 0, 7);
+
+		// Use a unique identifier key for this combination of string and settings.
+		return 'parse:' . $cache_id . '-' . md5(json_encode([
+			$string,
+			$input_types,
+			$output_type,
+			$options,
+			// Localization settings.
+			self::$encoding,
+			self::$locale,
+			self::$time_offset,
+			self::$time_format,
+			// BBCode settings.
+			self::getBBCodes(),
+			Config::$modSettings['disabledBBC'],
+			self::$enable_post_html,
+			// Smiley settings.
+			SmileyParser::loadData(self::$smiley_set),
+			// Additional stuff that might affect output.
+			$cache_key_extras,
+		]));
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/Sources/BBCodeParser.php b/Sources/Parsers/BBCodeParser.php
similarity index 86%
rename from Sources/BBCodeParser.php
rename to Sources/Parsers/BBCodeParser.php
index 2103fa2f8a..a02fffefb9 100644
--- a/Sources/BBCodeParser.php
+++ b/Sources/Parsers/BBCodeParser.php
@@ -13,36 +13,25 @@
 
 declare(strict_types=1);
 
-namespace SMF;
-
-use SMF\Cache\CacheApi;
-use SMF\Db\DatabaseApi as Db;
+namespace SMF\Parsers;
+
+use SMF\Attachment;
+use SMF\Autolinker;
+use SMF\BrowserDetector;
+use SMF\Config;
+use SMF\IntegrationHook;
+use SMF\Lang;
+use SMF\Parser;
+use SMF\Sapi;
+use SMF\Theme;
+use SMF\Time;
+use SMF\Url;
+use SMF\Utils;
 
 /**
  * Parses Bulletin Board Code in a string and converts it to HTML.
- *
- * The recommended way to use this class to parse BBCode in a string is:
- *
- *     $parsed_string = BBCodeParser::load()->parse($unparsed_string);
- *
- * Calling the load() method like this will save on memory by reusing a single
- * instance of the BBCodeParser class. However, if you need more control over
- * the parser, you can always instantiate a new one.
- *
- * The following integration hooks are called during object construction:
- *
- *     integrate_bbc_codes            (Used to add or modify BBC)
- *     integrate_smileys              (Used for alternative smiley handling)
- *
- * The following integration hooks are called during parsing:
- *
- *     integrate_pre_parsebbc         (Allows adjustments before parsing)
- *     integrate_post_parsebbc        (Gives access to results of parsing)
- *     integrate_attach_bbc_validate  (Adjusts HTML produced by the attach BBC)
- *     integrate_bbc_print            (For BBC that need special handling in
- *                                        print mode)
  */
-class BBCodeParser
+class BBCodeParser extends Parser
 {
 	/*******************
 	 * Public properties
@@ -55,20 +44,6 @@ class BBCodeParser
 	 */
 	public array $parse_tags = [];
 
-	/**
-	 * @var bool
-	 *
-	 * Whether BBCode should be parsed.
-	 */
-	public bool $enable_bbc;
-
-	/**
-	 * @var bool
-	 *
-	 * Whether to allow certain basic HTML tags in the input.
-	 */
-	public bool $enable_post_html;
-
 	/**
 	 * @var array
 	 *
@@ -76,69 +51,6 @@ class BBCodeParser
 	 */
 	public array $disabled = [];
 
-	/**
-	 * @var bool
-	 *
-	 * Whether smileys should be parsed.
-	 */
-	public bool $smileys = true;
-
-	/**
-	 * @var string
-	 *
-	 * The smiley set to use when parsing smileys.
-	 */
-	public string $smiley_set;
-
-	/**
-	 * @var bool
-	 *
-	 * Whether custom smileys are enabled.
-	 */
-	public bool $custom_smileys_enabled;
-
-	/**
-	 * @var string
-	 *
-	 * URL of the base smileys directory.
-	 */
-	public string $smileys_url;
-
-	/**
-	 * @var string
-	 *
-	 * The character encoding of the strings to be parsed.
-	 */
-	public string $encoding = 'UTF-8';
-
-	/**
-	 * @var bool
-	 *
-	 * Shorthand check for whether character encoding is UTF-8.
-	 */
-	public bool $utf8 = true;
-
-	/**
-	 * @var string
-	 *
-	 * Language locale to use.
-	 */
-	public string $locale = 'en_US';
-
-	/**
-	 * @var int
-	 *
-	 * User's time offset from UTC.
-	 */
-	public int $time_offset;
-
-	/**
-	 * @var string
-	 *
-	 * User's strftime format.
-	 */
-	public string $time_format;
-
 	/**
 	 * @var bool
 	 *
@@ -157,20 +69,6 @@ class BBCodeParser
 	 */
 	protected ?string $alltags_regex = null;
 
-	/**
-	 * @var ?string
-	 *
-	 * Regular expression to match smileys.
-	 */
-	protected ?string $smiley_preg_search = null;
-
-	/**
-	 * @var array
-	 *
-	 * Replacement values for smileys.
-	 */
-	protected array $smiley_preg_replacements = [];
-
 	/**
 	 * @var array
 	 *
@@ -272,13 +170,6 @@ class BBCodeParser
 	 */
 	private string $placeholder_template = "\u{E03C}" . '%1$s' . "\u{E03E}";
 
-	/**
-	 * @var array
-	 *
-	 * Holds parsed messages.
-	 */
-	private array $results = [];
-
 	/****************************
 	 * Internal static properties
 	 ****************************/
@@ -942,11 +833,11 @@ class BBCodeParser
 	private static bool $integrate_bbc_codes_done = false;
 
 	/**
-	 * @var self
+	 * @var array
 	 *
-	 * A reference to an existing, reusable instance of this class.
+	 * Reusable instances of this class.
 	 */
-	private static self $parser;
+	private static array $parsers = [];
 
 	/*****************
 	 * Public methods.
@@ -954,32 +845,12 @@ class BBCodeParser
 
 	/**
 	 * Constructor.
-	 *
-	 * @return object A reference to this object for method chaining.
-	 * @suppress PHP0436
 	 */
-	public function __construct()
+	public function __construct(bool $for_print = false)
 	{
-		// Set up localization.
-		if (!empty(Utils::$context['utf8'])) {
-			$this->utf8 = true;
-			$this->encoding = 'UTF-8';
-		} else {
-			$this->encoding = !empty(Config::$modSettings['global_character_set']) ? Config::$modSettings['global_character_set'] : (!empty(Lang::$txt['lang_character_set']) ? Lang::$txt['lang_character_set'] : $this->encoding);
+		$this->for_print = $for_print;
 
-			$this->utf8 = $this->encoding === 'UTF-8';
-		}
-
-		if (!empty(Lang::$txt['lang_locale'])) {
-			$this->locale = Lang::$txt['lang_locale'];
-		}
-
-		$this->time_offset = User::$me->time_offset ?? 0;
-		$this->time_format = User::$me->time_format ?? Time::getTimeFormat();
-
-		// Set up BBCode parsing.
-		$this->enable_bbc = !empty(Config::$modSettings['enableBBC']);
-		$this->enable_post_html = !empty(Config::$modSettings['enablePostHTML']);
+		parent::__construct();
 
 		self::integrateBBC();
 
@@ -987,27 +858,12 @@ public function __construct()
 			self::$codes,
 			fn ($a, $b) => $a['tag'] <=> $b['tag'],
 		);
-
-		// Set up smileys parsing.
-		$this->custom_smileys_enabled = !empty(Config::$modSettings['smiley_enable']);
-		$this->smileys_url = Config::$modSettings['smileys_url'];
-		$this->smiley_set = !empty(User::$me->smiley_set) ? User::$me->smiley_set : (!empty(Config::$modSettings['smiley_sets_default']) ? Config::$modSettings['smiley_sets_default'] : 'none');
-
-		// Maybe a mod wants to implement an alternative method for smileys
-		// (e.g. emojis instead of images)
-		if ($this->smiley_set !== 'none') {
-			IntegrationHook::call('integrate_smileys', [&$this->smiley_preg_search, &$this->smiley_preg_replacements]);
-		}
-
-		// Allow method chaining.
-		return $this;
 	}
 
 	/**
-	 * Parse bulletin board code in a string, as well as smileys optionally.
+	 * Parse bulletin board code in a string.
 	 *
 	 * @param string|bool $message The string to parse.
-	 * @param bool $smileys Whether to parse smileys. Default: true.
 	 * @param string|int $cache_id The cache ID.
 	 *    If $cache_id is left empty, an ID will be generated automatically.
 	 *    Manually specifying a ID is helpful in cases when an integration hook
@@ -1016,7 +872,7 @@ public function __construct()
 	 * @param array $parse_tags If set, only parses these tags rather than all of them.
 	 * @return string The parsed string.
 	 */
-	public function parse(string $message, bool $smileys = true, string|int $cache_id = '', array $parse_tags = []): string
+	public function parse(string $message, string|int $cache_id = '', array $parse_tags = []): string
 	{
 		// Don't waste cycles
 		if (strval($message) === '') {
@@ -1027,7 +883,6 @@ public function parse(string $message, bool $smileys = true, string|int $cache_i
 		$this->resetRuntimeProperties();
 
 		$this->message = $message;
-		$this->smileys = $smileys;
 		$this->parse_tags = $parse_tags;
 
 		$this->setDisabled();
@@ -1041,174 +896,16 @@ public function parse(string $message, bool $smileys = true, string|int $cache_i
 			return $this->message;
 		}
 
-		if (!$this->enable_bbc) {
-			if ($this->smileys === true) {
-				$this->message = $this->parseSmileys($this->message);
-			}
-
+		if (!self::$enable_bbc) {
 			$this->message = $this->fixHtml($this->message);
 
 			return $this->message;
 		}
 
-		// Allow mods access before entering $this->parseMessage.
-		IntegrationHook::call('integrate_pre_parsebbc', [&$this->message, &$this->smileys, &$cache_id, &$this->parse_tags, &$this->cache_key_extras]);
-
-		// If no cache id was given, make a generic one.
-		$cache_id = strval($cache_id) !== '' ? $cache_id : 'str' . substr(md5($this->message), 0, 7);
-
-		// Use a unique identifier key for this combination of string and settings.
-		$cache_key = 'parse:' . $cache_id . '-' . md5(json_encode([
-			$this->message,
-			// Localization settings.
-			$this->encoding,
-			$this->locale,
-			$this->time_offset,
-			$this->time_format,
-			// BBCode settings.
-			$this->bbc_codes,
-			$this->disabled,
-			$this->parse_tags,
-			$this->enable_post_html,
-			$this->for_print,
-			// Smiley settings.
-			$this->smileys,
-			$this->smiley_set,
-			$this->smiley_preg_search,
-			$this->smiley_preg_replacements,
-			// Additional stuff that might affect output.
-			$this->cache_key_extras,
-		]));
-
-		// Have we already parsed this string?
-		if (isset($this->results[$cache_key])) {
-			return $this->results[$cache_key];
-		}
-
-		// Or maybe we cached the results recently?
-		if (($this->results[$cache_key] = CacheApi::get($cache_key, 240)) != null) {
-			return $this->results[$cache_key];
-		}
-
-		// Keep track of how long this takes.
-		$cache_t = microtime(true);
-
 		// Do the job.
 		$this->parseMessage();
 
-		// Allow mods access to what $this->parseMessage created.
-		IntegrationHook::call('integrate_post_parsebbc', [&$this->message, &$this->smileys, &$cache_id, &$this->parse_tags]);
-
-		// Cache the output if it took some time...
-		if (!empty(CacheApi::$enable) && microtime(true) - $cache_t > pow(50, -CacheApi::$enable)) {
-			CacheApi::put($cache_key, $this->message, 240);
-		}
-
-		// Remember for later.
-		$this->results[$cache_key] = $this->message;
-
-		return $this->results[$cache_key];
-	}
-
-	/**
-	 * Parse smileys in the passed message.
-	 *
-	 * The smiley parsing function which makes pretty faces appear :).
-	 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
-	 * These are specifically not parsed in code tags [url=mailto:Dad@blah.com]
-	 * Caches the smileys from the database or array in memory.
-	 *
-	 * @param string $message The message to parse smileys in.
-	 * @return string The message with smiley images inserted.
-	 */
-	public function parseSmileys(string $message): string
-	{
-		if ($this->smiley_set == 'none' || trim($message) == '') {
-			return $message;
-		}
-
-		// If smileyPregSearch hasn't been set, do it now.
-		if (empty($this->smiley_preg_search)) {
-			// Cache for longer when customized smiley codes aren't enabled
-			$cache_time = !$this->custom_smileys_enabled ? 7200 : 480;
-
-			// Load the smileys in reverse order by length so they don't get parsed incorrectly.
-			if (($temp = CacheApi::get('parsing_smileys_' . $this->smiley_set, $cache_time)) == null) {
-				$smileysfrom = [];
-				$smileysto = [];
-				$smileysdescs = [];
-
-				$result = Db::$db->query(
-					'',
-					'SELECT s.code, f.filename, s.description
-					FROM {db_prefix}smileys AS s
-						JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
-					WHERE f.smiley_set = {string:smiley_set}' . (!$this->custom_smileys_enabled ? '
-						AND s.code IN ({array_string:default_codes})' : '') . '
-					ORDER BY LENGTH(s.code) DESC',
-					[
-						'default_codes' => ['>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'],
-						'smiley_set' => $this->smiley_set,
-					],
-				);
-
-				while ($row = Db::$db->fetch_assoc($result)) {
-					$smileysfrom[] = $row['code'];
-					$smileysto[] = Utils::htmlspecialchars($row['filename']);
-					$smileysdescs[] = !empty(Lang::$txt['icon_' . strtolower($row['description'])]) ? Lang::$txt['icon_' . strtolower($row['description'])] : $row['description'];
-				}
-				Db::$db->free_result($result);
-
-				CacheApi::put('parsing_smileys_' . $this->smiley_set, [$smileysfrom, $smileysto, $smileysdescs], $cache_time);
-			} else {
-				list($smileysfrom, $smileysto, $smileysdescs) = $temp;
-			}
-
-			// The non-breaking-space is a complex thing...
-			$non_breaking_space = $this->utf8 ? '\x{A0}' : '\xA0';
-
-			// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:David@bla.com] doesn't parse the :D smiley)
-			$this->smiley_preg_replacements = [];
-			$search_parts = [];
-			$smileys_path = Utils::htmlspecialchars($this->smileys_url . '/' . rawurlencode($this->smiley_set) . '/');
-
-			for ($i = 0, $n = count($smileysfrom); $i < $n; $i++) {
-				$special_chars = Utils::htmlspecialchars($smileysfrom[$i], ENT_QUOTES);
-
-				$smiley_code = '' . strtr($special_chars, [':' => ':', '(' => '(', ')' => ')', '$' => '$', '[' => '[']) . '';
-
-				$this->smiley_preg_replacements[$smileysfrom[$i]] = $smiley_code;
-
-				$search_parts[] = $smileysfrom[$i];
-
-				if ($smileysfrom[$i] != $special_chars) {
-					$this->smiley_preg_replacements[$special_chars] = $smiley_code;
-					$search_parts[] = $special_chars;
-
-					// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
-					$special_chars2 = preg_replace('/&#(\d{2});/', '�$1;', $special_chars);
-
-					if ($special_chars2 != $special_chars) {
-						$this->smiley_preg_replacements[$special_chars2] = $smiley_code;
-						$search_parts[] = $special_chars2;
-					}
-				}
-			}
-
-			$this->smiley_preg_search = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?utf8 ? 'u' : '');
-		}
-
-		// If there are no smileys defined, no need to replace anything
-		if (empty($this->smiley_preg_replacements)) {
-			return $message;
-		}
-
-		// Replace away!
-		return preg_replace_callback(
-			$this->smiley_preg_search,
-			fn ($matches) => $this->smiley_preg_replacements[$matches[1]],
-			$message,
-		);
+		return $this->message;
 	}
 
 	/**
@@ -1258,38 +955,6 @@ public function unparse(string $string): string
 		$string = preg_replace('~\\<\\!--.*?-->~i', '', $string);
 		$string = preg_replace('~\\<\\!\\[CDATA\\[.*?\\]\\]\\>~i', '', $string);
 
-		// Do the smileys ultra first!
-		preg_match_all('~]+alt="([^"]+)"[^>]+class="smiley"[^>]*>(?:\s)?~i', $string, $matches);
-
-		if (!empty($matches[0])) {
-			// Get all our smiley codes
-			$request = Db::$db->query(
-				'',
-				'SELECT code
-				FROM {db_prefix}smileys
-				ORDER BY LENGTH(code) DESC',
-				[],
-			);
-			$smiley_codes = Db::$db->fetch_all($request);
-			Db::$db->free_result($request);
-
-			foreach ($matches[1] as $k => $possible_code) {
-				$possible_code = Utils::htmlspecialcharsDecode($possible_code);
-
-				if (in_array($possible_code, $smiley_codes)) {
-					$matches[1][$k] = '-[]-smf_smily_start#|#' . $possible_code . '-[]-smf_smily_end#|#';
-				} else {
-					$matches[1][$k] = $matches[0][$k];
-				}
-			}
-
-			// Replace the tags!
-			$string = str_replace($matches[0], $matches[1], $string);
-
-			// Now sort out spaces
-			$string = str_replace(['-[]-smf_smily_end#|#-[]-smf_smily_start#|#', '-[]-smf_smily_end#|#', '-[]-smf_smily_start#|#'], ' ', $string);
-		}
-
 		// Only try to buy more time if the client didn't quit.
 		if (connection_aborted()) {
 			Sapi::resetTimeout();
@@ -2027,22 +1692,22 @@ public function getAllTagsRegex(): string
 	 * Using this method to get a BBCodeParser instance saves memory by avoiding
 	 * creating redundant instances.
 	 *
-	 * @param bool $init If true, reinitializes the reusable BBCodeParser.
+	 * @param bool $for_print If true, adjusts output for print media.
 	 * @return object An instance of this class.
 	 */
-	public static function load(bool $init = false): object
+	public static function load(bool $for_print = false): object
 	{
-		if (!isset(self::$parser) || !empty($init)) {
-			self::$parser = new self();
+		if (!isset(self::$parsers[(int) $for_print])) {
+			self::$parsers[(int) $for_print] = new self($for_print);
 		}
 
-		return self::$parser;
+		return self::$parsers[(int) $for_print];
 	}
 
 	/**
 	 * Get the list of supported BBCodes, including any added by modifications.
 	 *
-	 * @return array List of supported BBCodes
+	 * @return array List of supported BBCodes.
 	 */
 	public static function getCodes(): array
 	{
@@ -2052,9 +1717,10 @@ public static function getCodes(): array
 	}
 
 	/**
-	 * Returns an array of BBC tags that are allowed in signatures.
+	 * Returns an array of BBCodes tags that are allowed in signatures.
 	 *
-	 * @return array An array containing allowed tags for signatures, or an empty array if all tags are allowed.
+	 * @return array An array containing allowed tags for signatures, or an
+	 *    empty array if all tags are allowed.
 	 */
 	public static function getSigTags(): array
 	{
@@ -2066,7 +1732,7 @@ public static function getSigTags(): array
 
 		$disabled_tags = explode(',', $sig_bbc);
 
-		// Get all available bbc tags
+		// Get all available BBCode tags.
 		$temp = self::getCodes();
 		$allowed_tags = [];
 
@@ -2079,152 +1745,14 @@ public static function getSigTags(): array
 		$allowed_tags = array_unique($allowed_tags);
 
 		if (empty($allowed_tags)) {
-			// An empty array means that all bbc tags are allowed. So if all tags are disabled we need to add a dummy tag.
+			// An empty array means that all BBCode tags are allowed.
+			// So if all tags are disabled we need to add a dummy tag.
 			$allowed_tags[] = 'nonexisting';
 		}
 
 		return $allowed_tags;
 	}
 
-	/**
-	 * Highlight any code.
-	 *
-	 * Uses PHP's highlight_string() to highlight PHP syntax.
-	 * Does special handling to keep the tabs in the code available.
-	 * Used to parse PHP code from inside [code] and [php] tags.
-	 *
-	 * @param string $code The code.
-	 * @return string The code with highlighted HTML.
-	 */
-	public static function highlightPhpCode(string $code): string
-	{
-		// Remove special characters.
-		$code = Utils::htmlspecialcharsDecode(strtr($code, ['
' => "\n", '
' => "\n", "\t" => 'SMF_TAB();', '[' => '['])); - - $oldlevel = error_reporting(0); - - $buffer = str_replace(["\n", "\r"], '', @highlight_string($code, true)); - - error_reporting($oldlevel); - - // Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P. - $buffer = preg_replace('~SMF_TAB(?:<(?:font color|span style)="[^"]*?">)?\(\);~', '' . "\t" . '', $buffer); - - // PHP 8.3 changed the returned HTML. - $buffer = preg_replace('/^(
)?]*>|<\/code>(<\/pre>)?$/', '', $buffer);
-
-		return strtr($buffer, ['\'' => ''']);
-	}
-
-	/**
-	 * Microsoft uses their own character set Code Page 1252 (CP1252), which is
-	 * a superset of ISO 8859-1, defining several characters between DEC 128 and
-	 * 159 that are not normally displayable. This converts the popular ones
-	 * that appear from a cut and paste from Windows.
-	 *
-	 * @param string $string The string.
-	 * @return string The sanitized string.
-	 */
-	public static function sanitizeMSCutPaste(string $string): string
-	{
-		if (empty($string)) {
-			return $string;
-		}
-
-		self::load();
-
-		// UTF-8 occurrences of MS special characters.
-		$findchars_utf8 = [
-			"\xe2\x80\x9a",	// single low-9 quotation mark
-			"\xe2\x80\x9e",	// double low-9 quotation mark
-			"\xe2\x80\xa6",	// horizontal ellipsis
-			"\xe2\x80\x98",	// left single curly quote
-			"\xe2\x80\x99",	// right single curly quote
-			"\xe2\x80\x9c",	// left double curly quote
-			"\xe2\x80\x9d",	// right double curly quote
-		];
-
-		// windows 1252 / iso equivalents
-		$findchars_iso = [
-			chr(130),
-			chr(132),
-			chr(133),
-			chr(145),
-			chr(146),
-			chr(147),
-			chr(148),
-		];
-
-		// safe replacements
-		$replacechars = [
-			',',	// ‚
-			',,',	// „
-			'...',	// …
-			"'",	// ‘
-			"'",	// ’
-			'"',	// “
-			'"',	// ”
-		];
-
-		$string = str_replace(Utils::$context['utf8'] ? $findchars_utf8 : $findchars_iso, $replacechars, $string);
-
-		return $string;
-	}
-
-	/**
-	 * Backward compatibility wrapper for parse() and/or getCodes().
-	 *
-	 * @param string|bool $message The message.
-	 *		When an empty string, nothing is done.
-	 *		When false we provide a list of BBC codes available.
-	 *		When a string, the message is parsed and bbc handled.
-	 * @param bool $smileys Whether to parse smileys as well.
-	 * @param string $cache_id The cache ID.
-	 * @param array $parse_tags If set, only parses these tags rather than all of them.
-	 * @return string|array The parsed message or the list of BBCodes.
-	 */
-	public static function backcompatParseBbc(string|bool $message, bool $smileys = true, string $cache_id = '', array $parse_tags = []): string|array
-	{
-		if ($message === false) {
-			return self::getCodes();
-		}
-
-		self::load();
-
-		$cache_id = (is_string($cache_id) || is_int($cache_id)) && strlen($cache_id) === strspn($cache_id, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_') ? (string) $cache_id : '';
-
-		$for_print = self::$parser->for_print;
-		self::$parser->for_print = $smileys === 'print';
-
-		$message = self::$parser->parse((string) $message, !empty($smileys), $cache_id, (array) $parse_tags);
-
-		self::$parser->for_print = $for_print;
-
-		return $message;
-	}
-
-	/**
-	 * Backward compatibility wrapper for parseSmileys().
-	 * Doesn't return anything, but rather modifies $message directly.
-	 *
-	 * @param string &$message The message to parse smileys in.
-	 */
-	public static function backcompatParseSmileys(string &$message)
-	{
-		$message = self::load()->parseSmileys($message);
-	}
-
-	/**
-	 * Backward compatibility wrapper for unparse().
-	 *
-	 * @param string $string Text containing HTML
-	 * @return string The string with html converted to bbc
-	 */
-	public function htmlToBbc(string $string): string
-	{
-		return self::load()->unparse($string);
-	}
-
 	/*
 	 * BBCode validation methods.
 	 */
@@ -2727,20 +2255,7 @@ protected function parseMessage(): void
 			$this->message .= "\n" . $tag['after'] . "\n";
 		}
 
-		// Parse the smileys within the parts where it can be done safely.
-		if ($this->smileys === true) {
-			$message_parts = explode("\n", $this->message);
-
-			for ($i = 0, $n = count($message_parts); $i < $n; $i += 2) {
-				$message_parts[$i] = $this->parseSmileys($message_parts[$i]);
-			}
-
-			$this->message = implode('', $message_parts);
-		}
-		// No smileys, just get rid of the markers.
-		else {
-			$this->message = strtr($this->message, ["\n" => '']);
-		}
+		$this->message = strtr($this->message, ["\n" => '']);
 
 		// Transform the first table row into a table header and wrap the rest
 		// in table body tags.
@@ -2852,7 +2367,7 @@ protected function setDisabled(): void
 	protected function setBbcCodes(): void
 	{
 		// If we already have a version of the BBCodes for the current language, use that.
-		$locale_key = $this->locale . '|' . implode(',', $this->disabled);
+		$locale_key = self::$locale . '|' . implode(',', $this->disabled);
 
 		if (!empty($this->bbc_lang_locales[$locale_key])) {
 			$this->bbc_codes = $this->bbc_lang_locales[$locale_key];
@@ -3007,7 +2522,7 @@ function ($matches) {
 	 */
 	protected function fixHtml(string $data): string
 	{
-		if (empty($this->enable_post_html) || !str_contains($data, '<')) {
+		if (empty(self::$enable_post_html) || !str_contains($data, '<')) {
 			return $data;
 		}
 
@@ -3019,10 +2534,10 @@ function ($matches) {
 				}
 
 				if (str_starts_with($matches[2], Config::$boardurl)) {
-					return $this->enable_bbc ? '[iurl="' . $matches[2] . '"]' . $matches[3] . '[/iurl]' : '' . $matches[3] . '';
+					return self::$enable_bbc ? '[iurl="' . $matches[2] . '"]' . $matches[3] . '[/iurl]' : '' . $matches[3] . '';
 				}
 
-				return $this->enable_bbc ? '[url="' . $matches[2] . '"]' . $matches[3] . '[/url]' : '' . $matches[3] . '';
+				return self::$enable_bbc ? '[url="' . $matches[2] . '"]' . $matches[3] . '[/url]' : '' . $matches[3] . '';
 			},
 			$data,
 		);
@@ -4225,7 +3740,6 @@ protected function resetRuntimeProperties(): void
 		$to_reset = [
 			'message',
 			'bbc_codes',
-			'smileys',
 			'parse_tags',
 			'open_tags',
 			'inside',
@@ -4239,6 +3753,7 @@ protected function resetRuntimeProperties(): void
 		$class_vars = get_class_vars(__CLASS__);
 
 		foreach ($to_reset as $var) {
+			unset($this->{$var});
 			$this->{$var} = $class_vars[$var];
 		}
 	}
@@ -4266,7 +3781,7 @@ private static function integrateBBC(): void
 
 				// Closures cannot be serialized, but they can be reflected.
 				if (($value['validate'] ?? null) instanceof \Closure) {
-					$value['validate'] = (string) new ReflectionFunction($value['validate']);
+					$value['validate'] = (string) new \ReflectionFunction($value['validate']);
 				}
 
 				$serialized = serialize($value);
diff --git a/Sources/MarkdownParser.php b/Sources/Parsers/MarkdownParser.php
similarity index 98%
rename from Sources/MarkdownParser.php
rename to Sources/Parsers/MarkdownParser.php
index b9399bd1a3..ecf7f0053e 100644
--- a/Sources/MarkdownParser.php
+++ b/Sources/Parsers/MarkdownParser.php
@@ -13,7 +13,14 @@
 
 declare(strict_types=1);
 
-namespace SMF;
+namespace SMF\Parsers;
+
+use SMF\Autolinker;
+use SMF\Config;
+use SMF\IntegrationHook;
+use SMF\Lang;
+use SMF\Parser;
+use SMF\Utils;
 
 /**
  * Converts Markdown to BBCode or HTML.
@@ -33,7 +40,7 @@
  *
  *    SMF is capable of much more robust URI validation, so we use it.
  */
-class MarkdownParser
+class MarkdownParser extends Parser
 {
 	/*****************
 	 * Class constants
@@ -47,26 +54,7 @@ class MarkdownParser
 	 * Used to set the output to HTML rendered the same way that the reference
 	 * implementation of CommonMark would.
 	 */
-	public const OUTPUT_HTML_STRICT = 0;
-
-	/**
-	 * @var int
-	 *
-	 * Possible value for $this->output_type.
-	 *
-	 * Used to set the output to HTML rendered like the equivalent BBCode.
-	 * This is the default output type.
-	 */
-	public const OUTPUT_HTML = 1;
-
-	/**
-	 * @var int
-	 *
-	 * Possible value for $this->output_type.
-	 *
-	 * Used to convert Markdown into BBCode.
-	 */
-	public const OUTPUT_BBC = 2;
+	public const OUTPUT_HTML_STRICT = 3;
 
 	/**
 	 * @var int
@@ -725,8 +713,8 @@ class MarkdownParser
 	 * Constructor.
 	 *
 	 * @param int $output_type The type of output to generate.
-	 *    Value must be one of this class's OUTPUT_* constants.
-	 *    Default: self::OUTPUT_HTML.
+	 *    Value must be one of this class's or its parent class's OUTPUT_*
+	 *    constants. Default: self::OUTPUT_HTML.
 	 * @param ?int $hard_breaks How to handle line breaks in HTML output.
 	 *    Value should be a bitmask of this class's BR_* constants.
 	 *    If null, uses the value of Config::$modSettings['markdown_brs'].
@@ -735,13 +723,18 @@ class MarkdownParser
 	 */
 	public function __construct(int $output_type = self::OUTPUT_HTML, ?int $hard_breaks = null)
 	{
+		if ($output_type === self::OUTPUT_TEXT) {
+			$output_type === self::OUTPUT_HTML;
+		}
+
 		if (!in_array($output_type, [self::OUTPUT_BBC, self::OUTPUT_HTML, self::OUTPUT_HTML_STRICT])) {
 			throw new \ValueError();
 		}
 
-		$this->output_type = $output_type;
+		parent::__construct();
 
-		$this->hard_breaks = $hard_breaks ?? (int) (Config::$modSettings['markdown_brs'] ?? 0);
+		$this->hard_breaks = (int) ($hard_breaks ?? Config::$modSettings['markdown_brs'] ?? 0);
+		$this->output_type = $output_type;
 
 		// Maybe a mod wants to add a Markdown extension or something?
 		IntegrationHook::call('integrate_markdown', [&$this->block_types, &$this->render_methods]);
@@ -752,11 +745,11 @@ public function __construct(int $output_type = self::OUTPUT_HTML, ?int $hard_bre
 	 *
 	 * Getting Markdown and BBCode to play nicely with each other requires some
 	 * extra handling, so $from_bbcode_parser should always be set to true if
-	 * the string was already processed by the SMF\BBCodeParser class.
+	 * the string was already processed by the SMF\Parsers\BBCodeParser class.
 	 *
 	 * @param string $string The string to parse.
 	 * @param string $from_bbcode_parser Whether the string was the output from
-	 *    the SMF\BBCodeParser class.
+	 *    the SMF\Parsers\BBCodeParser class.
 	 * @return string The result of parsing the string.
 	 */
 	public function parse(string $string, bool $from_bbcode_parser = false): string
@@ -3988,21 +3981,35 @@ protected function getMethod(string|bool|null $method): mixed
 	 */
 	protected function resetRuntimeProperties(): void
 	{
-		foreach (get_class_vars($this::class) as $var => $value) {
-			if (in_array($var, ['output_type', 'hard_breaks', 'parsers'])) {
-				continue;
-			}
+		// Reset these properties.
+		$to_reset = [
+			'line_info',
+			'structure',
+			'open',
+			'last_block',
+			'link_reference_definitions',
+			'in_code',
+			'opening_fence_linenum',
+			'opening_fence',
+			'info_string',
+			'in_html',
+			'table_align',
+			'placeholders',
+			'rendered',
+		];
 
-			// Ensure p is always last.
-			if ($var === 'block_types') {
-				$p = $value['p'];
-				unset($value['p']);
-				$value['p'] = $p;
-			}
+		$class_vars = get_class_vars(__CLASS__);
 
+		foreach ($to_reset as $var) {
 			unset($this->{$var});
+			$this->{$var} = $class_vars[$var];
+		}
 
-			$this->{$var} = $value;
+		// Ensure p is always the last element in $this->block_types.
+		if (array_key_last($this->block_types) !== 'p') {
+			$p = $this->block_types['p'];
+			unset($this->block_types['p']);
+			$this->block_types['p'] = $p;
 		}
 	}
 }
diff --git a/Sources/Parsers/SmileyParser.php b/Sources/Parsers/SmileyParser.php
new file mode 100644
index 0000000000..7c8d34f96d
--- /dev/null
+++ b/Sources/Parsers/SmileyParser.php
@@ -0,0 +1,252 @@
+smiley_preg_replacements = [];
+			$search_parts = [];
+			$smileys_path = Utils::htmlspecialchars(self::$smileys_url . '/' . rawurlencode(self::$smiley_set) . '/');
+
+			foreach ($data as $id => $smiley) {
+				$special_chars = Utils::htmlspecialchars($smiley['code'], ENT_QUOTES);
+
+				$smiley_code = '' . strtr($special_chars, [':' => ':', '(' => '(', ')' => ')', '$' => '$', '[' => '[']) . '';
+
+				$this->smiley_preg_replacements[$smiley['code']] = $smiley_code;
+
+				$search_parts[] = $smiley['code'];
+
+				if ($smiley['code'] != $special_chars) {
+					$this->smiley_preg_replacements[$special_chars] = $smiley_code;
+					$search_parts[] = $special_chars;
+
+					// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
+					$special_chars2 = preg_replace('/&#(\d{2});/', '�$1;', $special_chars);
+
+					if ($special_chars2 != $special_chars) {
+						$this->smiley_preg_replacements[$special_chars2] = $smiley_code;
+						$search_parts[] = $special_chars2;
+					}
+				}
+			}
+
+			// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:David@bla.com] doesn't parse the :D smiley)
+			$this->smiley_preg_search = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?smiley_preg_search, &$this->smiley_preg_replacements]);
+		}
+	}
+
+	/**
+	 * Parse smileys in the passed string.
+	 *
+	 * The smiley parsing function which makes pretty faces appear :).
+	 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
+	 * These are specifically not parsed in code tags [url=mailto:Dad@blah.com]
+	 * Caches the smileys from the database or array in memory.
+	 *
+	 * @param string $string The string to parse smileys in.
+	 * @return string The string with smiley images inserted.
+	 */
+	public function parse(string $string): string
+	{
+		if (
+			self::$smiley_set == 'none'
+			|| !isset($this->smiley_preg_search)
+			|| empty($this->smiley_preg_replacements)
+			|| trim($string) == ''
+		) {
+			return $string;
+		}
+
+		// Don't parse smileys inside HTML or BBCode tags.
+		$parts = preg_split('~(<[^>]*>|\[\/?' . BBCodeParser::load()->getAllTagsRegex() . '[^\]]*\])~u', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+		for ($i = 0; $i < count($parts); $i++) {
+			if ($i % 2 === 0) {
+				$parts[$i] = preg_replace_callback(
+					$this->smiley_preg_search,
+					fn ($matches) => $this->smiley_preg_replacements[$matches[1]],
+					$parts[$i],
+				);
+			}
+		}
+
+		return implode('', $parts);
+	}
+
+	/**
+	 * Converts HTML img tags for smileys back into smiley text.
+	 *
+	 * @param string $string Text containing HTML.
+	 * @return string The string with smiley images converted to text.
+	 */
+	public function unparse(string $string): string
+	{
+		$smiley_codes = array_map(fn ($smiley) => $smiley['code'], self::loadData(''));
+
+		return preg_replace_callback(
+			'~(\h?)]+alt="([^"]+)"[^>]+class="smiley"[^>]*>(\h?)~i',
+			fn ($match) => in_array(html_entity_decode($match[2]), $smiley_codes) ? $match[1] . html_entity_decode($match[2]) . $match[3] : $match[0],
+			$string,
+		);
+	}
+
+	/************************
+	 * Public static methods.
+	 ************************/
+
+	/**
+	 * Returns a reusable instance of this class.
+	 *
+	 * Using this method to get a SmileyParser instance saves memory by avoiding
+	 * creating redundant instances.
+	 *
+	 * @return object An instance of this class.
+	 */
+	public static function load(): object
+	{
+		if (!isset(self::$parser)) {
+			self::$parser = new self();
+		}
+
+		return self::$parser;
+	}
+
+	/**
+	 * Loads data for the requested smiley set from the database.
+	 *
+	 * @param string $set The name of the smiley set.
+	 * @return array Data for all the smileys in the specified set.
+	 */
+	public static function loadData(string $set): array
+	{
+		if ($set === 'none') {
+			return [];
+		}
+
+		if (isset(self::$data[$set])) {
+			return self::$data[$set];
+		}
+
+		// Cache for longer when customized smiley codes aren't enabled
+		$cache_time = !self::$custom_smileys_enabled ? 7200 : 480;
+		$cache_key = 'parsing_smileys' . ($set !== '' ? '_' . $set : '');
+
+		if (is_array($data = CacheApi::get($cache_key, $cache_time))) {
+			self::$data[$set] = $data;
+
+			return self::$data[$set];
+		}
+
+		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
+		self::$data[$set] = [];
+
+		$request = Db::$db->query(
+			'',
+			'SELECT s.id_smiley, s.code, f.filename, s.description
+			FROM {db_prefix}smileys AS s
+				JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
+			WHERE ' . ($set !== '' ? 'f.smiley_set = {string:smiley_set}' : '1=1') . (!self::$custom_smileys_enabled ? '
+				AND s.code IN ({array_string:default_codes})' : '') . '
+			ORDER BY LENGTH(s.code) DESC',
+			[
+				'default_codes' => ['>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'],
+				'smiley_set' => $set,
+			],
+		);
+
+		while ($row = Db::$db->fetch_assoc($request)) {
+			self::$data[$set][(int) $row['id_smiley']] = [
+				'code' => $row['code'],
+				'filename' => $row['filename'],
+				'description' => !empty(Lang::$txt['icon_' . strtolower($row['description'])]) ? Lang::$txt['icon_' . strtolower($row['description'])] : $row['description'],
+			];
+		}
+
+		Db::$db->free_result($request);
+
+		CacheApi::put($cache_key, self::$data[$set], $cache_time);
+
+		return self::$data[$set];
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/Sources/Parsers/index.php b/Sources/Parsers/index.php
new file mode 100644
index 0000000000..976d292448
--- /dev/null
+++ b/Sources/Parsers/index.php
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/Sources/PersonalMessage/DraftPM.php b/Sources/PersonalMessage/DraftPM.php
index 15c8488d42..94b8d3fa55 100644
--- a/Sources/PersonalMessage/DraftPM.php
+++ b/Sources/PersonalMessage/DraftPM.php
@@ -15,13 +15,12 @@
 
 namespace SMF\PersonalMessage;
 
-use SMF\BBCodeParser;
 use SMF\Config;
 use SMF\Db\DatabaseApi as Db;
 use SMF\Draft;
 use SMF\Lang;
-use SMF\MarkdownParser;
 use SMF\PageIndex;
+use SMF\Parser;
 use SMF\Theme;
 use SMF\Time;
 use SMF\User;
@@ -273,11 +272,10 @@ public static function showInProfile(int $memID = -1): void
 			Lang::censorText($row['subject']);
 
 			// BBC-ilize the message.
-			$row['body'] = BBCodeParser::load()->parse($row['body'], true, 'draft' . $row['id_draft']);
-
-			if (!empty(Config::$modSettings['enableMarkdown'])) {
-				$row['body'] = MarkdownParser::load()->parse($row['body'], true);
-			}
+			$row['body'] = Parser::transform(
+				string: $row['body'],
+				options: ['cache_id' => 'draft' . $row['id_draft']],
+			);
 
 			// Have they provide who this will go to?
 			$recipients = [
diff --git a/Sources/PersonalMessage/PM.php b/Sources/PersonalMessage/PM.php
index 2ef1ee86f5..8b112bba12 100644
--- a/Sources/PersonalMessage/PM.php
+++ b/Sources/PersonalMessage/PM.php
@@ -19,7 +19,6 @@
 use SMF\Actions\PersonalMessage as PMAction;
 use SMF\ArrayAccessHelper;
 use SMF\Autolinker;
-use SMF\BBCodeParser;
 use SMF\Cache\CacheApi;
 use SMF\Config;
 use SMF\Db\DatabaseApi as Db;
@@ -29,9 +28,9 @@
 use SMF\IntegrationHook;
 use SMF\Lang;
 use SMF\Mail;
-use SMF\MarkdownParser;
 use SMF\Menu;
 use SMF\Msg;
+use SMF\Parser;
 use SMF\Security;
 use SMF\Theme;
 use SMF\Time;
@@ -403,11 +402,10 @@ public function format(int $counter = 0, array $format_options = []): array
 		}
 
 		// Run BBC interpreter on the message.
-		$this->formatted['body'] = BBCodeParser::load()->parse($this->formatted['body'], true, 'pm' . $this->id);
-
-		if (!empty(Config::$modSettings['enableMarkdown'])) {
-			$this->formatted['body'] = MarkdownParser::load()->parse($this->formatted['body'], true);
-		}
+		$this->formatted['body'] = Parser::transform(
+			string: $this->formatted['body'],
+			options: ['cache_id' => 'pm' . $this->id],
+		);
 
 		return $this->formatted;
 	}
@@ -538,7 +536,7 @@ public static function get(int|array $ids, array $query_customizations = []): \G
 		// There will never be an ID 0, but SMF doesn't like empty arrays when you tell it to expect an array of integers...
 		$params['ids'] = empty($ids) ? [0] : array_filter(array_unique(array_map('intval', $ids)));
 
-		foreach(self::queryData($selects, $params, $joins, $where, $order, $group, $limit) as $row) {
+		foreach (self::queryData($selects, $params, $joins, $where, $order, $group, $limit) as $row) {
 			$id = (int) $row['id_pm'];
 
 			yield (new self($id, $row));
@@ -1006,11 +1004,10 @@ public static function compose2(): bool
 			Msg::preparsecode($message);
 
 			// Make sure there's still some content left without the tags.
-			$temp = BBCodeParser::load()->parse(Utils::htmlspecialchars($message, ENT_QUOTES), false);
-
-			if (!empty(Config::$modSettings['enableMarkdown'])) {
-				$temp = MarkdownParser::load()->parse($temp, true);
-			}
+			$temp = Parser::transform(
+				string: Utils::htmlspecialchars($message, ENT_QUOTES),
+				input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN,
+			);
 
 			if (Utils::htmlTrim(strip_tags($temp, '')) === '' && (!User::$me->allowedTo('bbc_html') || !str_contains($message, '[html]'))) {
 				$post_errors[] = 'no_message';
@@ -1040,11 +1037,7 @@ public static function compose2(): bool
 			Msg::preparsecode(Utils::$context['preview_message'], true);
 
 			// Parse out the BBC if it is enabled.
-			Utils::$context['preview_message'] = BBCodeParser::load()->parse(Utils::$context['preview_message']);
-
-			if (!empty(Config::$modSettings['enableMarkdown'])) {
-				Utils::$context['preview_message'] = MarkdownParser::load()->parse(Utils::$context['preview_message'], true);
-			}
+			Utils::$context['preview_message'] = Parser::transform(Utils::$context['preview_message']);
 
 			// Censor, as always.
 			Lang::censorText(Utils::$context['preview_subject']);
@@ -1584,7 +1577,22 @@ public static function send(array $recipients, string $subject, string $message,
 
 					Lang::censorText($notification_texts[$lang]['body']);
 
-					$notification_texts[$lang]['body'] = trim(Utils::htmlspecialcharsDecode(strip_tags(strtr(BBCodeParser::load()->parse(Utils::htmlspecialchars($notification_texts[$lang]['body']), false), ['
' => "\n", '' => "\n", '' => "\n", '[' => '[', ']' => ']'])))); + $notification_texts[$lang]['body'] = Parser::transform( + string: Utils::htmlspecialchars($notification_texts[$lang]['body']), + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN, + output_type: Parser::OUTPUT_TEXT, + options: [ + 'text_replacements' => [ + '
' => "\n", + '' => "\n", + '' => "\n", + '[' => '[', + ']' => ']', + ], + ], + ); + + $notification_texts[$lang]['body'] = trim(Utils::htmlspecialcharsDecode($notification_texts[$lang]['body'])); } else { $notification_texts[$lang]['body'] = ''; } @@ -2126,11 +2134,10 @@ public static function reportErrors(array $error_types, array $named_recipients, Lang::censorText($row_quoted['subject']); Lang::censorText($row_quoted['body']); - $row['body'] = BBCodeParser::load()->parse($row_quoted['body'], true, 'pm' . $row_quoted['id_pm']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } + $row_quoted['body'] = Parser::transform( + string: $row_quoted['body'], + options: ['cache_id' => 'pm' . $row_quoted['id_pm']], + ); Utils::$context['quoted_message'] = [ 'id' => $row_quoted['id_pm'], @@ -2145,7 +2152,7 @@ public static function reportErrors(array $error_types, array $named_recipients, 'subject' => $row_quoted['subject'], 'time' => Time::create('@' . $row_quoted['msgtime'])->format(), 'timestamp' => $row_quoted['msgtime'], - 'body' => $row['body'], + 'body' => $row_quoted['body'], ]; } diff --git a/Sources/Poll.php b/Sources/Poll.php index cf6365645d..8dd588f377 100644 --- a/Sources/Poll.php +++ b/Sources/Poll.php @@ -362,16 +362,10 @@ public function format(array $format_options = []): array Lang::censorText($this->question); - $question = BBCodeParser::load()->parse($option->question); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $question = MarkdownParser::load()->parse($question, true); - } - $this->formatted = [ 'id' => $this->id ?? 0, 'image' => 'normal_' . (empty($this->voting_locked) ? 'poll' : 'locked_poll'), - 'question' => $question, + 'question' => Parser::transform($this->question), 'max_votes' => $this->max_votes, 'total_votes' => $this->total_voters, 'guest_vote' => $this->guest_vote, @@ -428,11 +422,7 @@ public function format(array $format_options = []): array $bar = round(($option->votes * 100) / $divisor, $precision); $barWide = $bar == 0 ? 1 : floor(($bar * 8) / 3); - $label = BBCodeParser::load()->parse($option->label); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $label = MarkdownParser::load()->parse($label, true); - } + $label = Parser::transform($option->label); // Now add it to the poll's contextual theme data. $this->formatted['choices'][$i] = [ diff --git a/Sources/Profile.php b/Sources/Profile.php index fc5ded1f15..54e7976f6e 100644 --- a/Sources/Profile.php +++ b/Sources/Profile.php @@ -1117,7 +1117,7 @@ public function loadCustomFields(string $area = 'summary'): void // Parse BBCode if ($cf_def['bbc']) { - $output_html = Utils::adjustHeadingLevels(BBCodeParser::load()->parse($output_html), null); + $output_html = Utils::adjustHeadingLevels(Parser::transform($output_html), null); } elseif ($cf_def['field_type'] == 'textarea') { // Allow for newlines at least $output_html = strtr($output_html, ["\n" => '
']); @@ -1350,11 +1350,15 @@ public function loadSignatureData(): bool Lang::censorText($signature); - Utils::$context['member']['signature_preview'] = Utils::adjustHeadingLevels(BBCodeParser::load()->parse($signature, true, 'sig' . $this->id, BBCodeParser::getSigTags()), null); + Utils::$context['member']['signature_preview'] = Parser::transform( + string: $signature, + options: [ + 'cache_id' => 'sig' . $this->id, + 'parse_tags' => Parser::getSigTags(), + ], + ); - if (!empty(Config::$modSettings['enableMarkdown'])) { - Utils::$context['member']['signature_preview'] = MarkdownParser::load()->parse(Utils::$context['member']['signature_preview'], true); - } + Utils::$context['member']['signature_preview'] = Utils::adjustHeadingLevels(Utils::$context['member']['signature_preview'], null); Utils::$context['member']['signature'] = $_POST['signature']; } @@ -1930,8 +1934,8 @@ public static function validateSignature(string &$value): bool|string return 'signature_max_image_count'; } - // What about too many smileys! - $smiley_parsed = BBCodeParser::load()->parseSmileys($unparsed_signature); + // What about too many smileys? + $smiley_parsed = Parser::transform($unparsed_signature, Parser::INPUT_SMILEYS); $smiley_count = substr_count(strtolower($smiley_parsed), 'prepareString() - */ - private BBCodeParser $bbcparser; - /**************** * Public methods ****************/ @@ -311,8 +303,7 @@ public function indexedWordQuery(array $words, array $search_data): mixed } } else { foreach ( - array_values(array_intersect_key($this->wildcard_words, array_flip($words['all_words']))) - as $key => $wildcard_word + array_values(array_intersect_key($this->wildcard_words, array_flip($words['all_words']))) as $key => $wildcard_word ) { $wildcard_word = !empty($this->params['ignore_accents']) ? $this->removeAccents($wildcard_word) : $this->escapeAccents($wildcard_word); @@ -1019,18 +1010,6 @@ protected function save(array $word_data): void */ protected function prepareString(string $string): string { - if (!isset($this->bbcparser)) { - // BBCodeParser complains if User::$me is not set. - if (!isset(User::$me)) { - User::setMe(0); - } - - $this->bbcparser = new BBCodeParser(); - - // Leave out anything that would be skipped for printing. - $this->bbcparser->for_print = true; - } - // Disable image proxy because we want the original URLs. $image_proxy_enabled = Config::$image_proxy_enabled ?? false; Config::$image_proxy_enabled = false; @@ -1045,13 +1024,18 @@ protected function prepareString(string $string): string $images = $_GET['images'] ?? null; unset($_GET['images']); - // Parse the BBCode. - $string = $this->bbcparser->parse($string, false); - - // Parse the Markdown. - if (!empty(Config::$modSettings['enableMarkdown'])) { - $string = MarkdownParser::load()->parse($string, true); - } + // Parse the BBCode and Markdown. + $string = Parser::transform( + string: $string, + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN, + output_type: Parser::OUTPUT_TEXT, + options: [ + 'for_print' => true, + 'preg_replace' => [ + '/<[^>]+>/' => fn ($matches) => $matches[0] . ' ', + ], + ], + ); // Put stuff back the way we found it. Config::$image_proxy_enabled = $image_proxy_enabled; @@ -1061,9 +1045,6 @@ protected function prepareString(string $string): string $_GET['images'] = $images; } - // Remove HTML. - $string = strip_tags(is_string($string) ? preg_replace('/<[^>]+>/', '$0 ', $string) : ''); - // Decode 4-byte Unicode characters. $string = mb_decode_numericentity($string, [0x010000, 0x10FFFF, 0, 0xFFFFFF], 'UTF-8'); diff --git a/Sources/Search/SearchApi.php b/Sources/Search/SearchApi.php index 1eac4d33bb..df7048ebc1 100644 --- a/Sources/Search/SearchApi.php +++ b/Sources/Search/SearchApi.php @@ -17,13 +17,13 @@ use SMF\Actions\Search; use SMF\BackwardCompatibility; -use SMF\BBCodeParser; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; use SMF\IntegrationHook; use SMF\Lang; use SMF\PackageManager\SubsPackage; +use SMF\Parser; use SMF\User; use SMF\Utils; @@ -1016,7 +1016,7 @@ protected function setBlacklistedWords(): void // Blacklist the BBC tags. $this->blacklisted_words = array_unique(array_merge( $this->blacklisted_words, - array_map(fn ($code) => $code['tag'], BBCodeParser::getCodes()), + array_map(fn ($code) => $code['tag'], Parser::getBBCodes()), )); IntegrationHook::call('integrate_search_blacklisted_words', [&$this->blacklisted_words]); diff --git a/Sources/Search/SearchResult.php b/Sources/Search/SearchResult.php index 51b6dcc3b2..749d9e2a53 100644 --- a/Sources/Search/SearchResult.php +++ b/Sources/Search/SearchResult.php @@ -16,12 +16,11 @@ namespace SMF\Search; use SMF\Autolinker; -use SMF\BBCodeParser; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\IP; use SMF\Lang; -use SMF\MarkdownParser; +use SMF\Parser; use SMF\Theme; use SMF\Time; use SMF\User; @@ -210,12 +209,14 @@ public function format(int $counter = 0, array $format_options = []): array // Set the number of characters before and after the searched keyword. $charLimit = 50; - if (!empty(Config::$modSettings['enableMarkdown'])) { - $this->body = MarkdownParser::load(MarkdownParser::OUTPUT_BBC)->parse($this->body); - } - $this->body = strtr($this->body, ["\n" => ' ', '
' => "\n", '
' => "\n", '
' => "\n"]); - $this->body = BBCodeParser::load()->parse($this->body, $this->smileys_enabled, $this->id_msg); + + $this->body = Parser::transform( + string: $this->body, + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ($this->smileys_enabled ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => $this->id_msg], + ); + $this->body = strip_tags(strtr($this->body, ['' => '
', '' => '
']), '
'); if (Utils::entityStrlen($this->body) > $charLimit) { @@ -259,11 +260,11 @@ public function format(int $counter = 0, array $format_options = []): array $this->body_highlighted = self::highlight($this->body, SearchApi::$loadedApi->searchArray); } else { // Run BBC interpreter on the message. - $this->body = BBCodeParser::load()->parse($this->body, $this->smileys_enabled, $this->id_msg); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $this->body = MarkdownParser::load()->parse($this->body, true); - } + $this->body = Parser::transform( + string: $this->body, + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ($this->smileys_enabled ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => $this->id_msg], + ); $this->subject_highlighted = self::highlight($this->subject, SearchApi::$loadedApi->searchArray); $this->body_highlighted = self::highlight($this->body, SearchApi::$loadedApi->searchArray); @@ -480,7 +481,7 @@ public static function get(int|array $ids, array $query_customizations = []): \G $params['message_list'] = self::$messages_to_get = array_filter(array_unique(array_map('intval', (array) $ids))); } - foreach(self::queryData($selects, $params, $joins, $where, $order, $group, $limit) as $row) { + foreach (self::queryData($selects, $params, $joins, $where, $order, $group, $limit) as $row) { $id = (int) $row['id_msg']; yield (new self($id, $row)); diff --git a/Sources/ServerSideIncludes.php b/Sources/ServerSideIncludes.php index cf93a82f0b..f9fd7c99dd 100644 --- a/Sources/ServerSideIncludes.php +++ b/Sources/ServerSideIncludes.php @@ -522,11 +522,11 @@ public static function queryPosts( $row['body'] = Autolinker::load(true)->makeLinks($row['body']); } - $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], $row['id_msg']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } + $row['body'] = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => (int) $row['id_msg']], + ); $row['body'] = strtr($row['body'], [Utils::TAB_SUBSTITUTE => '' . "\t" . '']); @@ -708,11 +708,18 @@ public static function recentTopics(int $num_recent = 8, ?array $exclude_boards $posts = []; while ($row = Db::$db->fetch_assoc($request)) { - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load(MarkdownParser::OUTPUT_BBC)->parse($row['body']); - } - - $row['body'] = strip_tags(strtr(BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], $row['id_msg']), ['
' => ' ', Utils::TAB_SUBSTITUTE => ' '])); + $row['body'] = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0), + output_type: Parser::OUTPUT_TEXT, + options: [ + 'cache_id' => (int) $row['id_msg'], + 'str_replace' => [ + '
' => ' ', + Utils::TAB_SUBSTITUTE => ' ', + ], + ], + ); if (Utils::entityStrlen($row['body']) > 128) { $row['body'] = Utils::entitySubstr($row['body'], 0, 128) . '...'; @@ -2249,11 +2256,11 @@ public static function boardNews(?int $board = null, ?int $limit = null, ?int $s $row['body'] .= '...'; } - $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], $row['id_msg']); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $row['body'] = MarkdownParser::load()->parse($row['body'], true); - } + $row['body'] = Parser::transform( + string: $row['body'], + input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0), + options: ['cache_id' => (int) $row['id_msg']], + ); $row['body'] = strtr($row['body'], [Utils::TAB_SUBSTITUTE => '' . "\t" . '']); diff --git a/Sources/Subs-Compat.php b/Sources/Subs-Compat.php index bca208ddbf..bd3151ba23 100644 --- a/Sources/Subs-Compat.php +++ b/Sources/Subs-Compat.php @@ -57,11 +57,9 @@ function Activate() } /** - * End - * Actions\Activate * Begin * Actions\Admin\ACP - * */ + */ function AdminMain() { return Actions\Admin\ACP::call(); @@ -108,9 +106,6 @@ function adminLogin(string $type = 'admin'): void } /** - * End - * Actions\Admin\ACP - * * Begin * Actions\Admin\AntiSpam */ @@ -120,9 +115,6 @@ function ModifyAntispamSettings(bool $return_config = false): ?array } /** - * End - * Actions\Admin\AntiSpam - * * Begin * Actions\Admin\Attachments */ @@ -192,9 +184,6 @@ function TransferAttachments(): void } /** - * End - * Actions\Admin\Attachments - * * Begin * Actions\Admin\Bans */ @@ -234,9 +223,6 @@ function BanLog(): void } /** - * End - * Actions\Admin\Bans - * * Begin * Actions\Admin\Boards */ @@ -251,9 +237,6 @@ function EditBoardSettings(bool $return_config = false): ?array } /** - * End - * Actions\Admin\Boards - * * Begin * Actions\Admin\Calendar */ @@ -278,9 +261,6 @@ function ModifyCalendarSettings(bool $return_config = false): void } /** - * End - * Actions\Admin\Calendar - * * Begin * Actions\Admin\EndSession */ @@ -290,9 +270,6 @@ function AdminEndSession(): void } /** - * End - * Actions\Admin\EndSession - * * Begin * Actions\Admin\ErrorLog */ @@ -302,9 +279,6 @@ function ViewErrorLog(): void } /** - * End - * Actions\Admin\ErrorLog - * * Begin * Actions\Admin\Features */ @@ -359,9 +333,6 @@ function ModifyAlertsSettings(): void } /** - * End - * Actions\Admin\Features - * * Begin * Actions\Admin\Find */ @@ -371,9 +342,6 @@ function AdminSearch(): void } /** - * End - * Actions\Admin\Find - * * Begin * Actions\Admin\Home */ @@ -383,9 +351,6 @@ function AdminHome(): void } /** - * End - * Actions\Admin\Home - * * Begin * Actions\Admin\Languages */ @@ -420,9 +385,6 @@ function ModifyLanguage(): void } /** - * End - * Actions\Admin\Languages - * * Begin * Actions\Admin\Logs */ @@ -432,9 +394,6 @@ function AdminLogs(bool $return_config = false): ?array } /** - * End - * Actions\Admin\Logs - * * Begin * Actions\Admin\Mail */ @@ -469,9 +428,6 @@ function TestMailSend(): void } /** - * End - * Actions\Admin\Mail - * * Begin * Actions\Admin\Maintenance */ @@ -608,9 +564,6 @@ function MaintainRemoveOldDrafts(): void } /** - * End - * Actions\Admin\Maintainence - * * Begin * Actions\Admin\Membergroups */ @@ -645,9 +598,6 @@ function ModifyMembergroupsettings(bool $return_config = false): ?array } /** - * End - * Actions\Admin\Membergoups - * * Begin * Actions\Admin\Members */ @@ -677,9 +627,6 @@ function SearchMembers(): void } /** - * End - * Actions\Admin\Members - * * Begin * Actions\Admin\Mods */ @@ -689,9 +636,6 @@ function ModifyModSettings(bool $return_config = false): ?array } /** - * End - * Actions\Admin\Mods - * * Begin * Actions\Admin\News */ @@ -731,9 +675,6 @@ function ModifyNewsSettings(bool $return_config = false): ?array } /** - * End - * Actions\Admin\News - * * Begin * Actions\Admin\Permissions */ @@ -828,9 +769,6 @@ function GeneralPermissionSettings(bool $return_config = false): ?array } /** - * End - * Actions\Admin\Permissions - * * Begin * Actions\Admin\Post */ @@ -855,9 +793,6 @@ function ModifyDraftSettings(bool $return_config = false): ?array } /** - * End - * Actions\Admin\Posts - * * Begin * Actions\Admin\Registration */ @@ -892,9 +827,6 @@ function ModifyRegistrationSettings(bool $return_config = false): ?array } /** - * End - * Actions\Admin\Registration - * * Begin * Actions\Admin\RepairBoards */ @@ -904,9 +836,6 @@ function RepairBoards(): void } /** - * End - * Actions\Admin\RepairBoards - * * Begin * Actions\Admin\Reports */ @@ -941,9 +870,6 @@ function StaffReport(): void } /** - * End - * Actions\Admin\Reports - * * Begin * Actions\Admin\Search */ @@ -973,9 +899,6 @@ function CreateMessageIndex(): void } /** - * End - * Actions\Admin\Search - * * Begin * Actions\Admin\SearchEngines */ @@ -1020,9 +943,6 @@ function EditSpider(): void } /** - * End - * Actions\Admin\SearchEngine - * * Begin * Actions\Admin\Server */ @@ -1087,9 +1007,6 @@ function ShowPHPinfoSettings(): void } /** - * End - * Actions\Admin\Server - * * Begin * Actions\Admin\Smileys */ @@ -1129,9 +1046,6 @@ function EditMessageIcons(): void } /** - * End - * Actions\Admin\Smileys - * * Begin * Actions\Admin\Subscriptions */ @@ -1196,9 +1110,6 @@ function ModifySubscriptionSettings(bool $return_config = false): ?array } /** - * End - * Actions\Admin\Subscriptions - * * Begin * Actions\Admin\Task */ @@ -1228,9 +1139,6 @@ function TaskSettings(bool $return_config = false): ?array } /** - * End - * Actions\Admin\Tasks - * * Begin * Actions\Admin\Themes */ @@ -1280,9 +1188,6 @@ function CopyTemplate(): void } /** - * End - * Actions\Admin\Themes - * * Begin * Actions\Admin\Warnings */ @@ -1292,9 +1197,6 @@ function ModifyWarningSettings(bool $return_config = false): ?array } /** - * End - * Actions\Admin\Warnings - * * Begin * Actions\Moderation\EndSession */ @@ -1304,9 +1206,6 @@ function ModEndSession(): void } /** - * End - * Actions\Moderation\EndSession - * * Begin * Actions\Moderation\Home */ @@ -1316,9 +1215,6 @@ function ModerationHome(): void } /** - * End - * Actions\Moderation\Home - * * Begin * Actions\Moderation\Logs */ @@ -1328,9 +1224,6 @@ function ViewModlog(): void } /** - * End - * Actions\Moderation\Logs - * * Begin * Actions\Moderation\Main */ @@ -1345,9 +1238,6 @@ function ModerationMail(bool $dont_call = false): void } /** - * End - * Actions\Moderation\Main - * * Begin * Actions\Moderation\Posts */ @@ -1377,9 +1267,6 @@ function ApproveMessage(): void } /** - * End - * Actions\Moderation\Posts - * * Begin * Actions\Moderation\ReportedContent */ @@ -1424,9 +1311,6 @@ function EditComment(): void } /** - * End - * Actions\Moderation\ReportedContent - * * Begin * Actions\Moderation\ShowNotice */ @@ -1436,9 +1320,6 @@ function ShowNotice(): void } /** - * End - * Actions\Moderation\ShowNotice - * * Begin * Actions\Moderation\Warnings */ @@ -1463,9 +1344,6 @@ function ModifyWarningTemplate(): void } /** - * End - * Actions\Moderation\Warnings - * * Begin * Actions\Moderation\WatchedUsers */ @@ -1475,9 +1353,6 @@ function ViewWatchedUsers(): void } /** - * End - * Actions\Moderation\WatchedUsers - * * Begin * Actions\Profile\Account */ @@ -1487,9 +1362,6 @@ function account(): void } /** - * End - * Actions\Profile\Account - * * Begin * Actions\Profile\Activate */ @@ -1499,9 +1371,6 @@ function activateAccount(): void } /** - * End - * Actions\Profile\Activate - * * Begin * Actions\Profile\AlertsPopup */ @@ -1511,9 +1380,6 @@ function alerts_popup(): void } /** - * End - * Actions\Profile\AlertsPopup - * * Begin * Actions\Profile\BuddyIgnoreLists */ @@ -1543,9 +1409,6 @@ function editIgnoreList(int $memID): void } /** - * End - * Actions\Profile\BuddyIgnoreLists - * * Begin * Actions\Profile\Delete */ @@ -1560,9 +1423,6 @@ function deleteAccount2(int $memID): void } /** - * End - * Actions\Profile\Delete - * * Begin * Actions\Profile\Export */ @@ -1582,9 +1442,6 @@ function get_export_formats(): array } /** - * End - * Actions\Profile\Export - * * Begin * Actions\Profile\ExportAttachment */ @@ -1594,9 +1451,6 @@ function export_attachment(): void } /** - * End - * Actions\Profile\ExportAttachment - * * Begin * Actions\Profile\ExportDownload */ @@ -1606,9 +1460,6 @@ function download_export_file(): void } /** - * End - * Actions\Profile\ExportDownload - * * Begin * Actions\Profile\ForumProfile */ @@ -1618,9 +1469,6 @@ function forumProfile(): void } /** - * End - * Actions\Profile\ForumProfile - * * Begin * Actions\Profile\GroupMembership */ @@ -1635,9 +1483,6 @@ function groupMembership2(int $memID): string } /** - * End - * Actions\Profile\GroupMembership - * * Begin * Actions\Profile\IgnoreBoards */ @@ -1647,9 +1492,6 @@ function ignoreboards(): void } /** - * End - * Actions\Profile\IgnoreBoards - * * Begin * Actions\Profile\IssueWarning */ @@ -1662,9 +1504,6 @@ function issueWarning(int $memID): void } /** - * End - * Actions\Profile\IssueWarning - * * Begin * Actions\Profile\Main */ @@ -1674,9 +1513,6 @@ function ModifyProfile(): void } /** - * End - * Actions\Profile\Main - * * Begin * Actions\Profile\Notification */ @@ -1728,9 +1564,6 @@ function makeNotificationChanges(int $memID): void } /** - * End - * Actions\Profile\Notification - * * Begin * Actions\Profile\PaidSubs */ @@ -1740,9 +1573,6 @@ function subscriptions(): void } /** - * End - * Actions\Profile\PaidSubs - * * Begin * Actions\Profile\Popup */ @@ -1752,9 +1582,6 @@ function profile_popup(): void } /** - * End - * Actions\Profile\Popup - * * Begin * Actions\Profile\ShowAlerts */ @@ -1767,9 +1594,6 @@ function showAlerts(int $memID): void } /** - * End - * Actions\Profile\ShowAlerts - * * Begin * Actions\Profile\ShowPermissions */ @@ -1782,9 +1606,6 @@ function showPermissions(int $memID): void } /** - * End - * Actions\Profile\ShowPermissions - * * Begin * Actions\Profile\ShowPost */ @@ -1804,9 +1625,6 @@ function showAttachments(int $memID): void } /** - * End - * Actions\Profile\ShowPosts - * * Begin * Actions\Profile\StatPanel */ @@ -1816,9 +1634,6 @@ function statPanel(int $memID): void } /** - * End - * Actions\Profile\StatPanel - * * Begin * Actions\Profile\Summary */ @@ -1828,9 +1643,6 @@ function summary(int $memID): void } /** - * End - * Actions\Profile\Summary - * * Begin * Actions\Profile\TFADisable */ @@ -1840,9 +1652,6 @@ function tfadisable(): void } /** - * End - * Actions\Profile\TFDADisable - * * Begin * Actions\Profile\TFASetup */ @@ -1852,9 +1661,6 @@ function tfasetup(): void } /** - * End - * Actions\Profile\TFASetup - * * Begin * Actions\Profile\ThemeOptions */ @@ -1864,9 +1670,6 @@ function theme(): void } /** - * End - * Actions\Profile\ThemeOptions - * * Begin * Actions\Profile\Tracking */ @@ -1900,9 +1703,6 @@ function TrackLogins(int $memID): void } /** - * End - * Actions\Profile\Tracking - * * Begin * Actions\Profile\ViewWarning */ @@ -1912,9 +1712,6 @@ function viewWarning(int $memID): void } /** - * End - * Actions\Profile\ViewWarning - * * Begin * Actions\Agreement */ @@ -1934,9 +1731,6 @@ function canRequirePrivacyPolicy(): bool } /** - * End - * Actions\Agreement - * * Begin * Actions\AgreementAccept */ @@ -1946,9 +1740,6 @@ function AcceptAgreement(): void } /** - * End - * Actions\AgreementAccept - * * Begin * Actions\Announce */ @@ -1968,9 +1759,6 @@ function AnnouncementSend(): void } /** - * End - * Actions\Announce - * * Begin * Actions\AttachmentApprove */ @@ -1980,9 +1768,6 @@ function ApproveAttach(): void } /** - * End - * Actions\AttachmentApprove - * * Begin * Actions\AttachmentDownload */ @@ -1992,9 +1777,6 @@ function showAttachment(): void } /** - * End - * Actions\AttachementDownload - * * Begin * Actions\AutoSuggest */ @@ -2023,9 +1805,6 @@ function AutoSuggest_Search_SMFVersions(): void } /** - * End - * Actions\AutoSuggest - * * Begin * Actions\BoardIndex */ @@ -2045,9 +1824,6 @@ function getBoardIndex(array $board_index_options): array } /** - * End - * Actions\BoardIndex - * * Begin * Actions\BuddyListToggle */ @@ -2057,9 +1833,6 @@ function BuddyListToggle(): void } /** - * End - * Actions\BuddyListToggle - * * Begin * Actions\Calendar */ @@ -2173,9 +1946,6 @@ function convertDateToEnglish(string $date): string } /** - * End - * Actions\Calendar - * * Begin * Actions\CoppaForm */ @@ -2185,9 +1955,6 @@ function CoppaForm(): void } /** - * End - * Actions\CoppaForm - * * Begin * Actions\Credits */ @@ -2197,9 +1964,6 @@ function Credits(bool $in_admin = false): void } /** - * End - * Actions\Credits - * * Begin * Actions\Display */ @@ -2209,9 +1973,6 @@ function Display(): void } /** - * End - * Actions\Display - * * Begin * Actions\DisplayAdminFile */ @@ -2221,9 +1982,6 @@ function DisplayAdminFile(): void } /** - * End - * Actions\DisplayAdminFile - * * Begin * Actions\Feed */ @@ -2243,9 +2001,6 @@ function cdata_parse(string $data, string $ns = '', bool $force = false): string } /** - * End - * Actions\Feed - * * Begin * Actions\FindMember * @deprecated @@ -2256,9 +2011,6 @@ function JSMembers(): void } /** - * End - * Actions\FindMember - * * Begin * Actions\Groups */ @@ -2288,9 +2040,6 @@ function GroupRequests(): void } /** - * End - * Actions\Groups - * * Begin * Actions\Help */ @@ -2305,9 +2054,6 @@ function HelpIndex(): void } /** - * End - * Actions\Help - * * Begin * Actions\HelpAdmin */ @@ -2317,9 +2063,6 @@ function ShowAdminHelp(): void } /** - * End - * Actions\HelpAdmin - * * Begin * Actions\JavaScriptModify */ @@ -2329,9 +2072,6 @@ function JavaScriptModify(): void } /** - * End - * Actions\JavaScriptModify - * * Begin * Actions\Login */ @@ -2341,9 +2081,6 @@ function Login(): void } /** - * End - * Actions\Login - * * Begin * Actions\Login2 */ @@ -2374,9 +2111,6 @@ function validatePasswordFlood( } /** - * End - * Actions\Login2 - * * Begin * Actions\LoginTFA */ @@ -2386,9 +2120,6 @@ function LoginTFA(): void } /** - * End - * Actions\LoginTFA - * * Begin * Actions\Logout */ @@ -2398,9 +2129,6 @@ function Logout(): void } /** - * End - * Actions\Logout - * * Begin * Actions\Memberlist */ @@ -2430,9 +2158,6 @@ function getCustFieldsMList(): array } /** - * End - * Actions\Memberlist - * * Begin * Actions\MessageIndex */ @@ -2452,9 +2177,6 @@ function buildTopicContext(array $row): void } /** - * End - * Actions\MessageIndex - * * Begin * Actions\MsgDelete */ @@ -2464,9 +2186,6 @@ function DeleteMessage(): void } /** - * End - * Actions\MsgDelete - * * Begin * Actions\Notify */ @@ -2496,9 +2215,6 @@ function createUnsubscribeToken(int $memID, string $email, string $type = '', in } /** - * End - * Actions\Notify - * * Begin * Actions\NotifyAnnouncements */ @@ -2508,9 +2224,6 @@ function AnnouncementsNotify(): void } /** - * End - * Actions\NotifyAnnouncements - * * Begin * Actions\NotifyBoard */ @@ -2520,9 +2233,6 @@ function BoardNotify(): void } /** - * End - * Actions\NotifyBoard - * * Begin * Actions\NotifyTopic */ @@ -2532,9 +2242,6 @@ function TopicNotify(): void } /** - * End - * Actions\NotifyTopic - * * Begin * Actions\PersonalMessage */ @@ -2614,9 +2321,6 @@ function MessageDrafts(): void } /** - * End - * Actions\PersonalMessage - * * Begin * Actions\Post */ @@ -2626,9 +2330,6 @@ function Post(): void } /** - * End - * Actions\Post - * * Begin * Actions\Post2 */ @@ -2638,9 +2339,6 @@ function Post2(): void } /** - * End - * Actions\Post2 - * * Begin * Actions\QuickModeration */ @@ -2650,9 +2348,6 @@ function QuickModeration(): void } /** - * End - * Actions\QuickModeration - * * Begin * Actions\QuickModerationInTopic */ @@ -2662,9 +2357,6 @@ function QuickInTopicModeration(): void } /** - * End - * Actions\QuickModerationInTopic - * * Begin * Actions\QuoteFast */ @@ -2674,9 +2366,6 @@ function QuoteFast(): void } /** - * End - * Actions\QuoteFast - * * Begin * Actions\Recent */ @@ -2691,9 +2380,6 @@ function getLastPost(): array } /** - * End - * Actions\Recent - * * Begin * Actions\Register */ @@ -2703,9 +2389,6 @@ function Register(array $reg_errors = []): void } /** - * End - * Actions\Register - * * Begin * Actions\Register2 */ @@ -2720,9 +2403,6 @@ function registerMember(array &$reg_options, bool $return_errors = false): int|a } /** - * End - * Actions\Register2 - * * Begin * Actions\Reminder */ @@ -2732,9 +2412,6 @@ function RemindMe(): void } /** - * End - * Actions\Reminder - * * Begin * Actions\ReportToMod */ @@ -2759,9 +2436,6 @@ function reportUser($id_member, $reason): void } /** - * End - * Actions\ReportToMod - * * Begin * Actions\RequestMembers */ @@ -2771,9 +2445,6 @@ function RequestMembers(): void } /** - * End - * Actions\RequestMembers - * * Begin * Actions\Search */ @@ -2783,9 +2454,6 @@ function PlushSearch1(): void } /** - * End - * Actions\Search - * * Begin * Actions\Search2 */ @@ -2795,9 +2463,6 @@ function PlushSearch2(): void } /** - * End - * Actions\Search2 - * * Begin * Actions\SendActivation */ @@ -2807,9 +2472,6 @@ function SendActivation(): void } /** - * End - * Actions\SendActivation - * * Begin * Actions\SmStats */ @@ -2819,9 +2481,6 @@ function SMStats(): void } /** - * End - * Actions\SmStats - * * Begin * Actions\Stats */ @@ -2831,9 +2490,6 @@ function DisplayStats(): void } /** - * End - * Actions\Stats - * * Begin * Actions\TopicMerge */ @@ -2858,9 +2514,6 @@ function MergeDone(): void } /** - * End - * Actions\TopicMerge - * * Begin * Actions\TopicMove */ @@ -2870,9 +2523,6 @@ function MoveTopic(): void } /** - * End - * Actions\TopicMove - * * Begin * Actions\TopicMove2 */ @@ -2887,9 +2537,6 @@ function moveTopicConcurrence() } /** - * End - * Actions\TopicMove2 - * * Begin * Actions\TopicPrint */ @@ -2899,9 +2546,6 @@ function PrintTopic(): void } /** - * End - * Actions\TopicPrint - * * Begin * Actions\TopicRemove */ @@ -2921,9 +2565,6 @@ function RemoveOldTopics2() } /** - * End - * Actions\TopicRemove - * * Begin * Actions\TopicRestore */ @@ -2933,9 +2574,6 @@ function RestoreTopic(): void } /** - * End - * Actions\TopicRestore - * * Begin * Actions\TopicSplit */ @@ -2970,9 +2608,6 @@ function SplitSelectionExecute(): void } /** - * End - * Actions\TopicSplit - * * Begin * Actions\TrackIP */ @@ -2983,9 +2618,6 @@ function TrackIP(int $memID = 0): void } /** - * End - * Actions\TrackIP - * * Begin * Actions\Unread */ @@ -2995,9 +2627,6 @@ function UnreadTopics(): void } /** - * End - * Actions\Unread - * * Begin * Actions\VerificationCode */ @@ -3007,9 +2636,6 @@ function VerificationCode(): void } /** - * End - * Actions\VerificationCode - * * Begin * Actions\ViewQUery */ @@ -3019,9 +2645,6 @@ function ViewQuery(): void } /** - * End - * Actions\ViewQUery - * * Begin * Actions\Who */ @@ -3036,9 +2659,6 @@ function determineActions(string|array $urls, string|bool $preferred_prefix = fa } /** - * End - * Actions\Who - * * Begin * Actions\XmlHttp */ @@ -3063,10 +2683,6 @@ function RetrievePreview(): void } /** - * End - * Actions\XmlHttp - * End Actions\* - * * Begin * Cache\CacheApi */ @@ -3101,9 +2717,6 @@ function cache_get_data(string $key, int $ttl = 120): mixed } /** - * End - * Cache\CacheApi - * * Begin * Db\DatabaseApi */ @@ -3118,9 +2731,6 @@ function db_extend() } /** - * End - * Db\DatabaseApi - * * Begin * Graphics\Image */ @@ -3211,9 +2821,6 @@ function resizeImage( } /** - * End - * Graphics\Image - * * Begin * Packagemanager\SubsPackage */ @@ -3413,9 +3020,6 @@ function package_validate_send(array $sendData): array } /** - * End - * PackageManager\SubsPackage - * * Begin * PersonalMessage\DraftPM */ @@ -3430,9 +3034,6 @@ function showInProfile(int $memID = -1): void } /** - * End - * PersonalMessage\DraftPM - * * Begin * PersonalMessage\PM */ @@ -3498,9 +3099,6 @@ function isAccessiblePM(int $pmID, string $folders = 'both'): bool } /** - * End - * PersonalMessage\PM - * * Begin * PersonalMessage\Rule */ @@ -3525,9 +3123,6 @@ function manage(): void } /** - * End - * PersonalMessage\Rule - * * Begin * Search\SearchApi */ @@ -3542,9 +3137,6 @@ function loadSearchAPIs(): array } /** - * End - * Search\SearchApi - * * Begin * Search\SearchResult */ @@ -3554,9 +3146,6 @@ function highlight(string $text, array $words): string } /** - * End - * Search\SearchResult - * * Begin * Unicode\Utf8String * @see SMF\BackwardCompatibility @@ -3627,9 +3216,6 @@ function utf8_sanitize_invisibles(string $string, int $level, string $substitute } /** - * End - * Unicode\Utf8String - * * Begin * WebFetch\WebFetchApi */ @@ -3639,9 +3225,6 @@ function fetch_web_data(string $url, string|array $post_data = [], bool $keep_al } /** - * End - * WebFetch\WebFetchApi - * * Begin * SMF\Alert */ @@ -3681,9 +3264,6 @@ function alert_purge(int $memID = 0, int $before = 0): void } /** - * End - * SMF\Alert - * * Begin * SMF\Attachment */ @@ -3788,50 +3368,61 @@ function getAttachmentFilename( } /** - * End - * SMF\Attachment - * * Begin - * SMF\BBCodeParser + * SMF\Parsers\BBCodeParser */ function get_signature_allowed_bbc_tags(): array { - return SMF\BBCodeParser::getSigTags(); + return SMF\Parser::getSigTags(); } function highlight_php_code(string $code): string { - return SMF\BBCodeParser::highlightPhpCode($code); + return SMF\Parser::highlightPhpCode($code); } function sanitizeMSCutPaste(string $string): string { - return SMF\BBCodeParser::sanitizeMSCutPaste($string); + return SMF\Parser::sanitizeMSCutPaste($string); } function parse_bbc( string|bool $message, - bool $smileys = true, + bool|string $smileys = true, string $cache_id = '', array $parse_tags = [], ): string|array { - return SMF\BBCodeParser::backcompatParseBbc( - $message, - $smileys, - $cache_id, - $parse_tags, + if ($message === false) { + return SMF\Parser::getBBCodes(); + } + + return SMF\Parser::transform( + string: $message, + input_types: SMF\Parser::INPUT_BBC | SMF\Parser::INPUT_MARKDOWN | (!empty($smileys) ? SMF\Parser::INPUT_SMILEYS : 0), + options: [ + 'cache_id' => $cache_id, + 'parse_tags' => $parse_tags, + 'for_print' => $smileys === 'print', + ], ); } function parseSmileys(string &$message): void { - SMF\BBCodeParser::backcompatParseSmileys($message); + $message = SMF\Parser::transform($message, SMF\Parser::INPUT_SMILEYS); + } + + function html_to_bbc(string $string): string + { + // We want to ignore Markdown in this backward compatibility function. + return SMF\Parser::transform( + string: $string, + input_types: SMF\Parser::INPUT_BBC | SMF\Parser::INPUT_SMILEYS, + output_type: SMF\Parser::OUTPUT_BBC, + ); } /** - * End - * SMF\BBCodeParser - * * Begin * SMF\Board */ @@ -3906,9 +3497,6 @@ function getBoardParents(int $id_parent): array } /** - * End - * SMF\Board - * * Begin * SMF\BrowserDetector */ @@ -3923,9 +3511,6 @@ function isBrowser(string $browser): bool } /** - * End - * SMF\BrowserDetector - * * Begin * SMF\Category */ @@ -3965,9 +3550,6 @@ function recursiveBoards(&$list, &$tree): void } /** - * End - * SMF\Category - * * Begin * SMF\Cookie */ @@ -4014,9 +3596,6 @@ function smf_setcookie( } /** - * End - * SMF\Cookie - * * Begin * SMF\Draft */ @@ -4036,9 +3615,6 @@ function showProfileDrafts(int $memID): void } /** - * End - * SMF\Draft - * * Begin * SMF\Editor */ @@ -4053,9 +3629,6 @@ function getMessageIcons(int $board_id): array } /** - * End - * SMF\Editor - * * Begin * SMF\ErrorHandler */ @@ -4104,9 +3677,6 @@ function display_loadavg_error(): void } /** - * End - * SMF\ErrorHandler - * * Begin * SMF\Event */ @@ -4126,9 +3696,6 @@ function removeEvent(int $id): void } /** - * End - * SMF\Event - * * Begin * SMF\Group */ @@ -4170,9 +3737,6 @@ function cache_getMembergroupList(): array } /** - * End - * SMF\Group - * * Begin * SMF\IntegrationHook */ @@ -4214,9 +3778,6 @@ function remove_integration_function( } /** - * End - * SMF\IntegrationHook - * * Begin * SMF\IP */ @@ -4267,9 +3828,6 @@ function expandIPv6(string $ip, bool $return_bool_if_invalid = true): string|boo } /** - * End - * SMF\IP - * * Begin * SMF\ItemList */ @@ -4279,9 +3837,6 @@ function createList(array $options): SMF\ItemList } /** - * End - * SMF\ItemList - * * Begin * SMF\Lang */ @@ -4320,9 +3875,6 @@ function comma_format(int|float $number, ?int $decimals = null): string } /** - * End - * SMF\Lang - * * Begin * SMF\Logging */ @@ -4367,9 +3919,6 @@ function displayDebug(): void } /** - * End - * SMF\Logging - * * Begin * SMF\Mail */ @@ -4470,9 +4019,6 @@ function loadEmailTemplate( } /** - * End - * SMF\Mail - * * Begin * SMF\Menu */ @@ -4487,9 +4033,6 @@ function destroyMenu(int|string $id = 'last'): void } /** - * End - * SMF\Menu - * * Begin * SMF\Msg */ @@ -4557,9 +4100,6 @@ function removeMessage(int $message, bool $decreasePostCount = true): bool } /** - * End - * SMF\Msg - * * Begin * SMF\PageIndex */ @@ -4582,9 +4122,6 @@ function constructPageIndex( } /** - * End - * SMF\PageIndex - * * Begin * SMF\Poll */ @@ -4619,9 +4156,6 @@ function RemovePoll(): void } /** - * End - * SMF\Poll - * * Begin * SMF\Profile */ @@ -4710,9 +4244,6 @@ function makeThemeChanges(int $id, int $id_theme): void } /** - * End - * SMF\Profile - * * Begin * SMF\QueryString */ @@ -4737,9 +4268,6 @@ function matchIPtoCIDR(string $ip_address, string $cidr_address): bool } /** - * End - * SMF\QueryString - * * Begin * SMF\Sapi */ @@ -4752,9 +4280,6 @@ function memoryReturnBytes(string $val): int return Sapi::memoryReturnBytes($val); } /** - * End - * SMF\Sapi - * * Begin * SMF\Security */ @@ -4809,9 +4334,6 @@ function KickGuest(): void } /** - * End - * SMF\Security - * * Begin * SMF\SecurityToken */ @@ -4831,9 +4353,6 @@ function cleanTokens(bool $complete = false): void } /** - * End - * SMF\SecurityToken - * * BEgin * SMF\ServerSideIncludes */ @@ -5093,9 +4612,6 @@ function ssi_recentAttachments(int $num_attachments = 10, array $attachment_ext } /** - * End - * SMF\ServerSideIncludes - * * Begin * SMF\Session */ @@ -5105,9 +4621,6 @@ function loadSession(): void } /** - * End - * SMF\Session - * * Begin * SMF\TaskRunner */ @@ -5117,9 +4630,6 @@ function CalculateNextTrigger(string|array $tasks = [], bool $force_update = fal } /** - * End - * SMF\TaskRunner - * * Begin * SMF\Theme */ @@ -5229,9 +4739,6 @@ function PickTheme(): void } /** - * End - * SMF\Theme - * * Begin * SMF\Time */ @@ -5267,9 +4774,6 @@ function forum_time(bool $use_user_offset = true, ?int $timestamp = null): int } /** - * End - * SMF\Time - * * Begin * SMF\TimeZone */ @@ -5299,9 +4803,6 @@ function validate_iso_country_codes(array|string $country_codes, bool $as_csv = } /** - * End - * SMF\TimeZone - * * Begin * SMF\Topic */ @@ -5340,9 +4841,6 @@ function prepareLikesContext(int $topic): array } /** - * End - * SMF\Topic - * * Begin * SMF\Url */ @@ -5412,9 +4910,6 @@ function httpsRedirectActive(string $url): bool } /** - * End - * SMF\Url - * * Begin * SMF\User */ @@ -5574,9 +5069,6 @@ function boardsAllowedTo(string|array $permission, bool $check_access = true, bo } /** - * End - * SMF\User - * * Begin * SMF\Utils */ @@ -5771,9 +5263,6 @@ function entity_fix__callback(array $matches): string } /** - * End - * SMF\Utils - * * Begin * SMF\Verifier */ @@ -5781,11 +5270,6 @@ function create_control_verification(array &$options, bool $do_test = false): bo { return SMF\Verifier::create($options, $do_test); } - - /* - * End - * BackwardCompatibility function map - */ } /*************************** diff --git a/Sources/Tasks/CreatePost_Notify.php b/Sources/Tasks/CreatePost_Notify.php index 1704628406..81cbbec92e 100644 --- a/Sources/Tasks/CreatePost_Notify.php +++ b/Sources/Tasks/CreatePost_Notify.php @@ -17,14 +17,13 @@ use SMF\Actions\Notify; use SMF\Alert; -use SMF\BBCodeParser; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; use SMF\Lang; use SMF\Mail; -use SMF\MarkdownParser; use SMF\Mentions; +use SMF\Parser; use SMF\TaskRunner; use SMF\Theme; use SMF\User; @@ -558,10 +557,11 @@ protected function handleWatchedNotifications(): void $localization = implode('|', [$member_data['lngfile'], $member_data['time_offset'], $member_data['time_format']]); if (empty($parsed_message[$localization])) { - $bbcparser = new BBCodeParser(); - $bbcparser->time_offset = $member_data['time_offset']; - $bbcparser->time_format = $member_data['time_format']; - $bbcparser->smiley_set = $member_data['smiley_set']; + // Use the target member's localization settings. + Parser::$time_offset = $member_data['time_offset']; + Parser::$time_format = $member_data['time_format']; + Parser::$smiley_set = $member_data['smiley_set']; + Parser::$locale = Lang::$txt['lang_locale']; $parsed_message[$localization]['subject'] = $msgOptions['subject']; $parsed_message[$localization]['body'] = $msgOptions['body']; @@ -569,13 +569,37 @@ protected function handleWatchedNotifications(): void Lang::censorText($parsed_message[$localization]['subject']); Lang::censorText($parsed_message[$localization]['body']); - if (!empty(Config::$modSettings['enableMarkdown'])) { - $parsed_message[$localization]['subject'] = MarkdownParser::load(MarkdownParser::OUTPUT_BBC)->parse($parsed_message[$localization]['subject'], false); - $parsed_message[$localization]['body'] = MarkdownParser::load(MarkdownParser::OUTPUT_BBC)->parse($parsed_message[$localization]['body'], false); + $parsed_message[$localization]['subject'] = Utils::htmlspecialcharsDecode($parsed_message[$localization]['subject']); + + $parsed_message[$localization]['body'] = strtr( + Parser::transform( + $parsed_message[$localization]['body'], + Parser::INPUT_BBC | Parser::INPUT_MARKDOWN, + ), + [ + '
' => "\n", + '' => "\n", + '' => "\n", + '[' => '[', + ']' => ']', + ''' => '\'', + '' => "\n", + '' => "\t", + '
' => "\n" . str_repeat('-', 63) . "\n", + ], + ); + + $parsed_message[$localization]['body'] = trim(Utils::htmlspecialcharsDecode(strip_tags($parsed_message[$localization]['body']))); + + // Go back to the default localization settings. + if (!isset(User::$me)) { + User::setMe(0); } - $parsed_message[$localization]['subject'] = Utils::htmlspecialcharsDecode($parsed_message[$localization]['subject']); - $parsed_message[$localization]['body'] = trim(Utils::htmlspecialcharsDecode(strip_tags(strtr($bbcparser->parse($parsed_message[$localization]['body'], false), ['
' => "\n", '' => "\n", '' => "\n", '[' => '[', ']' => ']', ''' => '\'', '' => "\n", '' => "\t", '
' => "\n---------------------------------------------------------------\n"])))); + Parser::$time_offset = User::$me->time_offset; + Parser::$time_format = User::$me->$time_format; + Parser::$smiley_set = (!empty(User::$me->smiley_set) ? User::$me->smiley_set : (!empty(Config::$modSettings['smiley_sets_default']) ? Config::$modSettings['smiley_sets_default'] : 'none')); + Parser::$locale = Lang::getLocaleFromLanguageName(User::$me->$language); } // Bitwise check: Receiving an alert? diff --git a/Sources/Tasks/ExportProfileData.php b/Sources/Tasks/ExportProfileData.php index fa289f6bbc..70d1757feb 100644 --- a/Sources/Tasks/ExportProfileData.php +++ b/Sources/Tasks/ExportProfileData.php @@ -912,6 +912,7 @@ public function execute(): bool // Use some temporary integration hooks to manipulate BBC parsing during export. $hook_methods = [ + 'parser_cache' => 'parser_cache', 'pre_parsebbc' => in_array($this->_details['format'], ['HTML', 'XML_XSLT']) ? 'pre_parsebbc_html' : 'pre_parsebbc_xml', 'post_parsebbc' => 'post_parsebbc', 'bbc_codes' => 'bbc_codes', @@ -1877,16 +1878,23 @@ public static function add_dtd( ]); } + /** + * Adds data to the cache key to distinguish parsing for exports from normal + * parsing. + */ + public static function parser_cache(array &$cache_key_extras): void + { + $cache_key_extras[__CLASS__] = 1; + } + /** * Adjusts some parse_bbc() parameters for the special case of HTML and * XML_XSLT exports. */ - public static function pre_parsebbc_html(string &$message, bool &$smileys, string &$cache_id, array &$parse_tags, array &$cache_key_extras): void + public static function pre_parsebbc_html(string &$message, bool &$smileys, string &$cache_id, array &$parse_tags): void { $cache_id = ''; - $cache_key_extras[__CLASS__] = 1; - foreach (['smileys_url', 'attachmentThumbnails'] as $var) { if (isset(Config::$modSettings[$var])) { self::$real_modSettings[$var] = Config::$modSettings[$var]; @@ -1900,12 +1908,10 @@ public static function pre_parsebbc_html(string &$message, bool &$smileys, strin /** * Adjusts some parse_bbc() parameters for the special case of XML exports. */ - public static function pre_parsebbc_xml(string &$message, bool &$smileys, string &$cache_id, array &$parse_tags, array &$cache_key_extras): void + public static function pre_parsebbc_xml(string &$message, bool &$smileys, string &$cache_id, array &$parse_tags): void { $cache_id = ''; - $cache_key_extras[__CLASS__] = 1; - $smileys = false; if (!isset(Config::$modSettings['disabledBBC'])) { diff --git a/Sources/Theme.php b/Sources/Theme.php index a89bdbfd44..5e5db5389f 100644 --- a/Sources/Theme.php +++ b/Sources/Theme.php @@ -774,7 +774,12 @@ public static function setupContext(bool $forceload = false): void } // Clean it up for presentation ;). - Utils::$context['news_lines'][$i] = Utils::adjustHeadingLevels(BBCodeParser::load()->parse(stripslashes(trim(Utils::$context['news_lines'][$i])), true, 'news' . $i), null); + Utils::$context['news_lines'][$i] = Parser::transform( + string: stripslashes(trim(Utils::$context['news_lines'][$i])), + options: ['cache_id' => 'news' . $i], + ); + + Utils::$context['news_lines'][$i] = Utils::adjustHeadingLevels(Utils::$context['news_lines'][$i], null); } if (!empty(Utils::$context['news_lines']) && (!empty(Config::$modSettings['allow_guestAccess']) || User::$me->is_logged)) { @@ -3005,7 +3010,7 @@ protected static function templateInclude(string $filename, bool $once = false): // I know, I know... this is VERY COMPLICATED. Still, it's good. if (preg_match('~ (\d+)$~i', $error, $match) != 0) { $data = file($filename); - $data2 = BBCodeParser::highlightPhpCode(implode('', $data)); + $data2 = Parser::highlightPhpCode(implode('', $data)); $data2 = preg_split('~\\~', $data2); // Fix the PHP code stuff... diff --git a/Sources/User.php b/Sources/User.php index 5d7c386044..3695e28e89 100644 --- a/Sources/User.php +++ b/Sources/User.php @@ -1218,11 +1218,13 @@ public function format(bool $display_custom_fields = false): array Lang::censorText($this->formatted['blurb']); Lang::censorText($this->formatted['signature']); - $this->formatted['signature'] = BBCodeParser::load()->parse(str_replace(["\n", "\r"], ['
', ''], $this->formatted['signature']), true, 'sig' . $this->id, BBCodeParser::getSigTags()); - - if (!empty(Config::$modSettings['enableMarkdown'])) { - $this->formatted['signature'] = MarkdownParser::load()->parse($this->formatted['signature'], true); - } + $this->formatted['signature'] = Parser::transform( + string: str_replace(["\n", "\r"], ['
', ''], $this->formatted['signature']), + options: [ + 'cache_id' => 'sig' . $this->id, + 'parse_tags' => Parser::getSigTags(), + ], + ); $this->formatted['signature'] = Utils::adjustHeadingLevels($this->formatted['signature'], null); } @@ -1258,7 +1260,7 @@ public function format(bool $display_custom_fields = false): array // BBC? if ($custom['bbc']) { - $value = Utils::adjustHeadingLevels(BBCodeParser::load()->parse($value), null); + $value = Utils::adjustHeadingLevels(Parser::transform($value), null); } // ... or checkbox? elseif (isset($custom['type']) && $custom['type'] == 'check') { @@ -3017,7 +3019,7 @@ public static function updateMemberData(int|array|null $members, array $data): v foreach ($data as $var => $val) { switch ($var) { - case 'birthdate': + case 'birthdate': $type = 'date'; break; diff --git a/Sources/Verifier.php b/Sources/Verifier.php index 7ca26947c1..87c2c65c8a 100644 --- a/Sources/Verifier.php +++ b/Sources/Verifier.php @@ -626,7 +626,7 @@ protected function setQuestions(): void $this->questions[] = [ 'id' => $q, - 'q' => Utils::adjustHeadingLevels(BBCodeParser::load()->parse($row['question']), null), + 'q' => Utils::adjustHeadingLevels(Parser::transform($row['question']), null), 'is_error' => !empty($incorrectQuestions) && in_array($q, $incorrectQuestions), // Remember a previous submission? 'a' => isset($_REQUEST[$this->id . '_vv'], $_REQUEST[$this->id . '_vv']['q'], $_REQUEST[$this->id . '_vv']['q'][$q]) ? Utils::htmlspecialchars($_REQUEST[$this->id . '_vv']['q'][$q]) : '', diff --git a/other/upgrade_2-1_MySQL.sql b/other/upgrade_2-1_MySQL.sql index da91c8dd63..f7779a37a2 100644 --- a/other/upgrade_2-1_MySQL.sql +++ b/other/upgrade_2-1_MySQL.sql @@ -201,8 +201,8 @@ if (version_compare(trim(strtolower(@Config::$modSettings['smfVersion'])), '2.1. while ($row = Db::$db->fetch_assoc($request)) { $inserts[] = array( - 'name' => Utils::htmlspecialchars(strip_tags(SMF\BBCodeParser::load()->unparse($row['name']))), - 'description' => Utils::htmlspecialchars(strip_tags(SMF\BBCodeParser::load()->unparse($row['description']))), + 'name' => Utils::htmlspecialchars(strip_tags(SMF\Parser::transform($row['name'], SMF\Parser::OUTPUT_BBC))), + 'description' => Utils::htmlspecialchars(strip_tags(SMF\Parser::transform($row['description'], SMF\Parser::OUTPUT_BBC))), 'id' => $row['id'], ); } diff --git a/other/upgrade_2-1_PostgreSQL.sql b/other/upgrade_2-1_PostgreSQL.sql index 96e2fedced..0343dcdc2b 100644 --- a/other/upgrade_2-1_PostgreSQL.sql +++ b/other/upgrade_2-1_PostgreSQL.sql @@ -392,8 +392,8 @@ if (version_compare(trim(strtolower(@Config::$modSettings['smfVersion'])), '2.1. while ($row = Db::$db->fetch_assoc($request)) { $inserts[] = array( - 'name' => Utils::htmlspecialchars(strip_tags(SMF\BBCodeParser::load()->unparse($row['name']))), - 'description' => Utils::htmlspecialchars(strip_tags(SMF\BBCodeParser::load()->unparse($row['description']))), + 'name' => Utils::htmlspecialchars(strip_tags(SMF\Parser::transform($row['name'], SMF\Parser::OUTPUT_BBC)), + 'description' => Utils::htmlspecialchars(strip_tags(SMF\Parser::transform($row['description'], SMF\Parser::OUTPUT_BBC)), 'id' => $row['id'], ); }