diff --git a/Languages/en_US/Admin.php b/Languages/en_US/Admin.php index 0710ebc8ef..30719de852 100644 --- a/Languages/en_US/Admin.php +++ b/Languages/en_US/Admin.php @@ -586,9 +586,10 @@ $txt['manageposts_settings'] = 'Post Settings'; $txt['manageposts_settings_description'] = 'Here you can set everything related to posts and posting.'; -$txt['manageposts_bbc_settings'] = 'Bulletin Board Code'; -$txt['manageposts_bbc_settings_description'] = 'Bulletin board code can be used to add markup to forum messages. For example, to highlight the word "house" you can type [b]house[/b]. All Bulletin board code tags are surrounded by square brackets ("[" and "]").'; +$txt['manageposts_bbc_settings'] = 'Bulletin Board Code & Markdown'; +$txt['manageposts_bbc_settings_description'] = 'Bulletin board code and Markdown can be used to add markup to forum messages. For example, to highlight the word "house" you can type [b]house[/b] (using BBC) or **house** (using Markdown). All bulletin board code tags are surrounded by square brackets ("[" and "]"). Markdown syntax is a little more complicated, but not too hard.'; $txt['manageposts_bbc_settings_title'] = 'Bulletin Board Code settings'; +$txt['manageposts_markdown_settings_title'] = 'Markdown settings'; $txt['manageposts_topic_settings'] = 'Topic Settings'; $txt['manageposts_topic_settings_description'] = 'Here you can set all settings involving topics.'; @@ -630,6 +631,10 @@ $txt['enabled_bbc_select_all'] = 'Select all tags'; $txt['groups_can_use'] = 'Membergroups allowed to use {0}'; +$txt['enableMarkdown'] = 'Enable Markdown'; +$txt['collapse_blank_lines'] = 'Collapse extra blank lines'; +$txt['collapse_single_breaks'] = 'Clean up line breaks inside paragraphs'; + $txt['enableParticipation'] = 'Enable participation icons'; $txt['oldTopicDays'] = 'Time before topic is warned as old on reply'; $txt['defaultMaxTopics'] = 'Number of topics per page in the message index'; diff --git a/Languages/en_US/Editor.php b/Languages/en_US/Editor.php index 05b0c64641..a1b67e8cbf 100644 --- a/Languages/en_US/Editor.php +++ b/Languages/en_US/Editor.php @@ -33,6 +33,7 @@ $editortxt['insert_table'] = 'Insert a table'; $editortxt['insert_horizontal_rule'] = 'Insert a horizontal rule'; $editortxt['code'] = 'Code'; +$editortxt['tt'] = 'Inline code'; $editortxt['insert_quote'] = 'Insert a Quote'; $editortxt['width'] = 'Width (optional):'; $editortxt['height'] = 'Height (optional):'; @@ -60,5 +61,6 @@ $editortxt['float_right'] = 'Float right'; $editortxt['maximize'] = 'Maximize'; $editortxt['dateformat'] = 'month/day/year'; +$editortxt['heading'] = 'Heading'; ?> \ No newline at end of file diff --git a/Languages/en_US/Help.php b/Languages/en_US/Help.php index 987e3aac4c..b63990d39f 100644 --- a/Languages/en_US/Help.php +++ b/Languages/en_US/Help.php @@ -337,6 +337,10 @@
  • <pre>, <blockquote>
  • '; +$helptxt['enableMarkdown'] = 'Enabling this setting will allow your members to use Markdown throughout the forum, providing them with an alternative syntax to format their posts.

    Note that, unlike pure Markdown, SMF’s implementation does not allow authors to embed raw HTML into their posts except as permitted by the "Enable basic HTML in posts" setting.'; +$helptxt['collapse_blank_lines'] = 'Enabling this setting will remove unnecessary blank lines in post content, resulting in more consistent formatting in the output. Leave it disabled to preserve the blank lines.

    This feature is only available when Markdown support is enabled.'; +$helptxt['collapse_single_breaks'] = 'Enabling this setting will remove single line breaks inside paragraphs, resulting in more consistent formatting in the output. Leave it disabled to preserve the line breaks.

    Even when this feature is enabled, authors can still force line breaks to happen by using the [br] BBCode or by ending the line with a backslash character or with two or more spaces.

    This feature is only available when Markdown support is enabled.'; + $helptxt['themes_manage'] = 'Here you can install new themes and select which themes your users can choose from, the default theme that new users and guests will use, as well as other theme selection settings.'; $helptxt['theme_install'] = 'This allows you to install new themes. You can do this from an existing directory, by uploading an archive for the theme, or by copying the default theme.

    Note that the archive or directory must have a
    theme_info.xml
    definition file.'; $helptxt['xmlnews_enable'] = 'Allows people to link to Recent news 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 f8b8684095..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; @@ -318,11 +318,11 @@ public function view(): void } elseif ($this->filter['variable'] == 'url') { Utils::$context['filter']['value']['html'] = '\'' . strtr(Utils::htmlspecialchars((str_starts_with($this->filter['value']['sql'], '?') ? Config::$scripturl : '') . $this->filter['value']['sql']), ['\\_' => '_']) . '\''; } elseif ($this->filter['variable'] == 'message') { - Utils::$context['filter']['value']['html'] = '\'' . strtr(Utils::htmlspecialchars($this->filter['value']['sql']), ["\n" => '
    ', '<br />' => '
    ', "\t" => '   ', '\\_' => '_', '\\%' => '%', '\\\\' => '\\']) . '\''; + Utils::$context['filter']['value']['html'] = '\'' . strtr(Utils::htmlspecialchars($this->filter['value']['sql']), ["\n" => '
    ', '<br />' => '
    ', "\t" => Utils::TAB_SUBSTITUTE, '\\_' => '_', '\\%' => '%', '\\\\' => '\\']) . '\''; Utils::$context['filter']['value']['html'] = preg_replace('~&lt;span class=&quot;remove&quot;&gt;(.+?)&lt;/span&gt;~', '$1', Utils::$context['filter']['value']['html']); } elseif ($this->filter['variable'] == 'error_type') { - Utils::$context['filter']['value']['html'] = '\'' . strtr(Utils::htmlspecialchars($this->filter['value']['sql']), ["\n" => '
    ', '<br />' => '
    ', "\t" => '   ', '\\_' => '_', '\\%' => '%', '\\\\' => '\\']) . '\''; + Utils::$context['filter']['value']['html'] = '\'' . strtr(Utils::htmlspecialchars($this->filter['value']['sql']), ["\n" => '
    ', '<br />' => '
    ', "\t" => Utils::TAB_SUBSTITUTE, '\\_' => '_', '\\%' => '%', '\\\\' => '\\']) . '\''; } else { Utils::$context['filter']['value']['html'] = &$this->filter['value']['sql']; } @@ -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 4c6329c0c0..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; @@ -28,6 +27,8 @@ use SMF\ItemList; use SMF\Lang; use SMF\Menu; +use SMF\Parser; +use SMF\Parsers\MarkdownParser; use SMF\Profile; use SMF\Sapi; use SMF\SecurityToken; @@ -165,6 +166,10 @@ public function bbc(): void // Legacy BBC are listed separately, but we use the same info in both cases Config::$modSettings['bbc_disabled_legacyBBC'] = Config::$modSettings['bbc_disabled_disabledBBC']; + // The Markdown settings for handling line breaks are actually a single bitmask. + Config::$modSettings['collapse_blank_lines'] = (int) !((Config::$modSettings['markdown_brs'] ?? 0) & MarkdownParser::BR_LINES); + Config::$modSettings['collapse_single_breaks'] = (int) !((Config::$modSettings['markdown_brs'] ?? 0) & MarkdownParser::BR_IN_PARAGRAPHS); + $extra = ''; if (isset($_REQUEST['cowsay'])) { @@ -180,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'])) { @@ -189,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]); @@ -239,6 +244,12 @@ function ($config_var) { }, ); + // Save the Markdown collapse_* settings as a bitmask. + $config_vars[] = ['int', 'markdown_brs']; + $_POST['markdown_brs'] = (!empty($_POST['collapse_blank_lines']) ? 0 : MarkdownParser::BR_LINES); + $_POST['markdown_brs'] |= (!empty($_POST['collapse_single_breaks']) ? 0 : MarkdownParser::BR_IN_PARAGRAPHS); + unset($_POST['collapse_blank_lines'], $_POST['collapse_single_breaks']); + IntegrationHook::call('integrate_save_bbc_settings', [$bbcTags]); ACP::saveDBSettings($config_vars); @@ -581,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']; } @@ -1024,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); @@ -1613,6 +1624,12 @@ public static function bbcConfigVars(): array // This one is actually pretend... ['bbc', 'legacyBBC', 'help' => 'legacy_bbc'], + + // Markdown settings + ['title', 'markdown_settings', 'text_label' => Lang::$txt['manageposts_markdown_settings_title']], + ['check', 'enableMarkdown', 'onchange' => 'document.getElementById(\'collapse_blank_lines\').disabled = !this.checked; document.getElementById(\'collapse_single_breaks\').disabled = !this.checked;'], + ['check', 'collapse_blank_lines', 'disabled' => empty(Config::$modSettings['enableMarkdown'])], + ['check', 'collapse_single_breaks', 'disabled' => empty(Config::$modSettings['enableMarkdown'])], ]; // Permissions for restricted BBC diff --git a/Sources/Actions/Admin/News.php b/Sources/Actions/Admin/News.php index 8d9f454534..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; @@ -197,7 +197,7 @@ function addNewsItem () ' + last_preview + ' . - '{js_escape:" style="overflow: auto; width: 100%; height: 10ex;"> + '{js_escape:" style="overflow: auto; width: 100%; min-height: 10ex;"> }' . @@ -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)), ]; } @@ -1141,7 +1141,7 @@ public static function list_getNewsTextarea(array $news): string */ public static function list_getNewsPreview(array $news): string { - return '
    ' . $news['parsed'] . '
    '; + return '
    ' . Utils::adjustHeadingLevels($news['parsed'], null) . '
    '; } /** @@ -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 fb75deacfb..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]); } @@ -1671,7 +1671,7 @@ public function install(): void Utils::$context['is_installed'] = false; Utils::$context['package_name'] = $smileyInfo['name']; - loadTemplate('Packages'); + Theme::loadTemplate('Packages'); } // Do the actual install else { @@ -2248,7 +2248,7 @@ protected function __construct() User::$me->isAllowedTo('manage_smileys'); Lang::load('ManageSmileys'); - loadTemplate('ManageSmileys'); + Theme::loadTemplate('ManageSmileys'); // If customized smileys is disabled don't show the setting page if (empty(Config::$modSettings['smiley_enable'])) { diff --git a/Sources/Actions/Agreement.php b/Sources/Actions/Agreement.php index d798804595..be325340fe 100644 --- a/Sources/Actions/Agreement.php +++ b/Sources/Actions/Agreement.php @@ -17,10 +17,10 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Config; use SMF\ErrorHandler; use SMF\Lang; +use SMF\Parser; use SMF\Theme; use SMF\User; use SMF\Utils; @@ -148,7 +148,10 @@ 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); } @@ -157,9 +160,15 @@ protected function prepareAgreementContext(): void 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']) { diff --git a/Sources/Actions/Announce.php b/Sources/Actions/Announce.php index bc8dfb3056..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,6 +26,7 @@ use SMF\Lang; use SMF\Logging; use SMF\Mail; +use SMF\Parser; use SMF\Theme; use SMF\Topic; use SMF\User; @@ -167,7 +167,9 @@ public function send(): void Lang::censorText(Utils::$context['topic_subject']); Lang::censorText($message); - $message = trim(Utils::htmlspecialcharsDecode(strip_tags(strtr(BBCodeParser::load()->parse($message, false, $id_msg), ['
    ' => "\n", '' => "\n", '' => "\n", '[' => '[', ']' => ']'])))); + $message = Parser::transform(string: $message, options: ['cache_id' => $id_msg]); + + $message = trim(Utils::htmlspecialcharsDecode(strip_tags(strtr($message, ['
    ' => "\n", '' => "\n", '' => "\n", '

    ' => '', '

    ' => "\n\n", '[' => '[', ']' => ']'])))); // Select the email addresses for this batch. $announcements = []; diff --git a/Sources/Actions/Feed.php b/Sources/Actions/Feed.php index 61fe7c2980..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,6 +28,7 @@ use SMF\IntegrationHook; use SMF\IP; use SMF\Lang; +use SMF\Parser; use SMF\Sapi; use SMF\Theme; use SMF\Time; @@ -778,7 +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']); + $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']); @@ -1218,7 +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']); + $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']); @@ -1982,7 +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']); + $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'])) { @@ -2433,7 +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']); + $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 5642bbae25..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; @@ -28,6 +27,7 @@ use SMF\Lang; use SMF\Logging; use SMF\Msg; +use SMF\Parser; use SMF\Time; use SMF\Topic; use SMF\User; @@ -137,7 +137,12 @@ public function execute(): void Msg::preparsecode($_POST['message']); - if (Utils::htmlTrim(strip_tags(BBCodeParser::load()->parse($_POST['message'], false), implode('', Utils::$context['allowed_html_tags']))) === '') { + $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'; unset($_POST['message']); } @@ -294,7 +299,13 @@ 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']); + 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); } // Topic? elseif (empty($post_errors)) { 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 3736ea9f92..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; @@ -26,6 +25,7 @@ use SMF\IntegrationHook; use SMF\Lang; use SMF\PageIndex; +use SMF\Parser; use SMF\Theme; use SMF\Time; use SMF\User; @@ -243,9 +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 - $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']) . '...'; } @@ -259,7 +264,12 @@ public static function buildTopicContext(array $row): void $row['last_subject'] = $row['first_subject']; $row['last_body'] = $row['first_body']; } else { - $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 1a89bff88f..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,13 +295,15 @@ protected function notes(): void Utils::$context['notes'] = []; foreach ($moderator_notes as $note) { + $note['body'] = Parser::transform($note['body']); + Utils::$context['notes'][] = [ 'author' => [ 'id' => $note['id_member'], 'link' => $note['id_member'] ? ('' . $note['member_name'] . '') : $note['member_name'], ], 'time' => Time::create('@' . $note['log_time'])->format(), - 'text' => BBCodeParser::load()->parse($note['body']), + 'text' => $note['body'], 'delete_href' => Config::$scripturl . '?action=moderate;area=index;notes;delete=' . $note['id_note'] . ';' . Utils::$context['session_var'] . '=' . Utils::$context['session_id'], 'can_delete' => User::$me->allowedTo('admin_forum') || $note['id_member'] == User::$me->id, ]; diff --git a/Sources/Actions/Moderation/Posts.php b/Sources/Actions/Moderation/Posts.php index d953a98a41..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; @@ -30,6 +29,7 @@ use SMF\Menu; use SMF\Msg; use SMF\PageIndex; +use SMF\Parser; use SMF\SecurityToken; use SMF\Theme; use SMF\Time; @@ -379,13 +379,19 @@ public function posts(): void $can_delete = false; } + $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'], 'counter' => Utils::$context['start'] + $i, 'href' => Config::$scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . '#msg' . $row['id_msg'], 'link' => '' . $row['subject'] . '', 'subject' => $row['subject'], - 'body' => BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], (int) $row['id_msg']), + 'body' => $row['body'], 'time' => Time::create('@' . $row['poster_time'])->format(), 'poster' => [ 'id' => $row['id_member'], @@ -777,6 +783,8 @@ public static function list_getUnapprovedAttachments(int $start, int $items_per_ ); while ($row = Db::$db->fetch_assoc($request)) { + $row['body'] = Parser::transform($row['body']); + $unapproved_items[] = [ 'id' => $row['id_attach'], 'filename' => $row['filename'], @@ -791,7 +799,7 @@ public static function list_getUnapprovedAttachments(int $start, int $items_per_ 'message' => [ 'id' => $row['id_msg'], 'subject' => $row['subject'], - 'body' => BBCodeParser::load()->parse($row['body']), + 'body' => $row['body'], 'time' => Time::create('@' . $row['poster_time'])->format(), 'href' => Config::$scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . '#msg' . $row['id_msg'], ], diff --git a/Sources/Actions/Moderation/ReportedContent.php b/Sources/Actions/Moderation/ReportedContent.php index 4fd1b89af0..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; @@ -30,6 +29,7 @@ use SMF\Logging; use SMF\Menu; use SMF\PageIndex; +use SMF\Parser; use SMF\SecurityToken; use SMF\Theme; use SMF\Time; @@ -265,7 +265,7 @@ public function details(): void 'href' => Config::$scripturl . '?action=profile;u=' . $report['id_author'], ], 'subject' => $report['subject'], - 'body' => BBCodeParser::load()->parse($report['body']), + 'body' => Parser::transform($report['body']), ]; } @@ -929,6 +929,7 @@ protected function getReports(int $closed = 0): array ]; } else { $report_boards_ids[] = $row['id_board']; + $extraDetails = [ 'topic' => [ 'id' => $row['id_topic'], @@ -943,7 +944,7 @@ protected function getReports(int $closed = 0): array 'href' => Config::$scripturl . '?action=profile;u=' . $row['id_author'], ], 'subject' => $row['subject'], - 'body' => BBCodeParser::load()->parse($row['body']), + 'body' => Parser::transform($row['body']), ]; } @@ -1137,7 +1138,7 @@ protected function getReportComments(int $report_id): array while ($row = Db::$db->fetch_assoc($request)) { $report['mod_comments'][] = [ 'id' => $row['id_comment'], - 'message' => BBCodeParser::load()->parse($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 0e01347866..857be78872 100644 --- a/Sources/Actions/Moderation/ShowNotice.php +++ b/Sources/Actions/Moderation/ShowNotice.php @@ -17,10 +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\Parser; use SMF\Theme; use SMF\User; use SMF\Utils; @@ -59,7 +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); + 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 434e870b21..733c83667b 100644 --- a/Sources/Actions/Moderation/WatchedUsers.php +++ b/Sources/Actions/Moderation/WatchedUsers.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\ItemList; use SMF\Lang; use SMF\Menu; use SMF\Msg; +use SMF\Parser; use SMF\Theme; use SMF\Time; use SMF\User; @@ -427,12 +427,18 @@ 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'] = 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'], 'id_topic' => $row['id_topic'], 'author_link' => '' . $row['real_name'] . '', 'subject' => $row['subject'], - 'body' => BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], (int) $row['id_msg']), + 'body' => $row['body'], 'poster_time' => Time::create('@' . $row['poster_time'])->format(), 'approved' => $row['approved'], 'can_delete' => $delete_boards == [0] || in_array($row['id_board'], $delete_boards), diff --git a/Sources/Actions/Post.php b/Sources/Actions/Post.php index bac705621f..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; @@ -30,6 +29,7 @@ use SMF\IntegrationHook; use SMF\Lang; use SMF\Msg; +use SMF\Parser; use SMF\Poll; use SMF\Security; use SMF\Theme; @@ -453,7 +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']); + $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]); @@ -953,7 +957,11 @@ 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'])); + 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']); if ($this->form_subject != '') { diff --git a/Sources/Actions/Post2.php b/Sources/Actions/Post2.php index e38416c312..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; @@ -31,6 +30,7 @@ use SMF\Lang; use SMF\Logging; use SMF\Msg; +use SMF\Parser; use SMF\Poll; use SMF\Search\SearchApi; use SMF\Security; @@ -268,7 +268,12 @@ public function submit(): void Msg::preparsecode($_POST['message']); // Let's see if there's still some content left without the tags. - if (Utils::htmlTrim(strip_tags(BBCodeParser::load()->parse($_POST['message'], false), implode('', Utils::$context['allowed_html_tags']))) === '' && (!User::$me->allowedTo('bbc_html') || !str_contains($_POST['message'], '[html]'))) { + $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 d69241c7ec..58fe724f3b 100644 --- a/Sources/Actions/Profile/IssueWarning.php +++ b/Sources/Actions/Profile/IssueWarning.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\ItemList; use SMF\Lang; use SMF\Msg; +use SMF\Parser; use SMF\PersonalMessage\PM; use SMF\Profile; use SMF\Time; @@ -537,7 +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); + $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 1c9ca033fe..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; @@ -30,6 +29,7 @@ use SMF\Menu; use SMF\Msg; use SMF\PageIndex; +use SMF\Parser; use SMF\Profile; use SMF\Theme; use SMF\Time; @@ -838,7 +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']); + $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 f043699e8a..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 ? BBCodeParser::load()->parse($extra['previous']) : $extra['previous']) : '', - 'after' => !empty($extra['new']) ? ($parse_bbc ? BBCodeParser::load()->parse($extra['new']) : $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 ab7cecc967..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; @@ -27,6 +26,7 @@ use SMF\Lang; use SMF\Msg; use SMF\PageIndex; +use SMF\Parser; use SMF\Theme; use SMF\Time; use SMF\User; @@ -174,7 +174,11 @@ public static function getLastPost(): array Lang::censorText($row['subject']); Lang::censorText($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 b0b3e9e41d..ffec4c6af0 100644 --- a/Sources/Actions/Register.php +++ b/Sources/Actions/Register.php @@ -17,10 +17,10 @@ use SMF\ActionInterface; use SMF\ActionTrait; -use SMF\BBCodeParser; use SMF\Config; use SMF\ErrorHandler; use SMF\Lang; +use SMF\Parser; use SMF\Profile; use SMF\SecurityToken; use SMF\Theme; @@ -183,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'] = ''; } @@ -224,9 +236,15 @@ 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'); diff --git a/Sources/Actions/TopicPrint.php b/Sources/Actions/TopicPrint.php index 0c23363670..49e7afe35e 100644 --- a/Sources/Actions/TopicPrint.php +++ b/Sources/Actions/TopicPrint.php @@ -18,12 +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\Parser; use SMF\Poll; use SMF\Theme; use SMF\Time; @@ -96,14 +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; - - // 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; @@ -138,12 +131,14 @@ public function execute(): void Lang::censorText($row['subject']); Lang::censorText($row['body']); + $row['body'] = Parser::transform($row['body'], options: ['for_print' => true]); + Utils::$context['posts'][] = [ 'subject' => $row['subject'], 'member' => $row['poster_name'], 'time' => Time::create('@' . $row['poster_time'])->format(null, false), 'timestamp' => $row['poster_time'], - 'body' => $bbcparser->parse($row['body']), + 'body' => $row['body'], 'id_msg' => $row['id_msg'], ]; diff --git a/Sources/Actions/TopicSplit.php b/Sources/Actions/TopicSplit.php index b3f5bacfe0..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; @@ -31,6 +30,7 @@ use SMF\Mail; use SMF\Msg; use SMF\PageIndex; +use SMF\Parser; use SMF\Search\SearchApi; use SMF\Theme; use SMF\Time; @@ -458,7 +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']); + $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'], @@ -503,7 +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']); + $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 f18354edb2..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; @@ -27,6 +26,7 @@ use SMF\IntegrationHook; use SMF\Lang; use SMF\Msg; +use SMF\Parser; use SMF\Profile; use SMF\Theme; use SMF\User; @@ -167,7 +167,7 @@ public function newspreview(): void 'identifier' => 'parsedNews', 'children' => [ [ - 'value' => BBCodeParser::load()->parse($news), + 'value' => Utils::adjustHeadingLevels(Parser::transform($news), null), ], ], ], @@ -237,9 +237,21 @@ 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, + ], + ); + + $current_signature = Utils::adjustHeadingLevels($current_signature, null); + } $preview_signature = !empty($_POST['signature']) ? Utils::htmlspecialchars($_POST['signature']) : Lang::$txt['no_signature_preview']; @@ -251,7 +263,15 @@ public function sig_preview(): void Lang::censorText($preview_signature); - $preview_signature = BBCodeParser::load()->parse($preview_signature, true, 'sig' . $user, $allowedTags); + $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) { if ($is_owner) { $errors[] = ['value' => Lang::$txt['cannot_profile_extra_own'], 'attributes' => ['type' => 'error']]; @@ -354,7 +374,8 @@ 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); + $warning_body = Parser::transform($warning_body); + $warning_body = Utils::adjustHeadingLevels($warning_body, null); } Utils::$context['preview_message'] = $warning_body; diff --git a/Sources/Autolinker.php b/Sources/Autolinker.php index f06b1205e1..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(); } /** @@ -785,12 +785,15 @@ public static function createJavaScriptFile(bool $force = false): void $regexes = self::load()->getJavaScriptUrlRegexes(); $regexes['email'] = self::load()->getJavaScriptEmailRegex(); + // Don't autolink if the URL is inside a Markdown link construct. + $md_lookbehind = !empty(Config::$modSettings['enableMarkdown']) ? '(? $value) { - $js[] = 'autolinker_regexes.set(' . Utils::escapeJavaScript($key) . ', new RegExp(' . Utils::escapeJavaScript($value) . ', "giu"));'; + $js[] = 'autolinker_regexes.set(' . Utils::escapeJavaScript($key) . ', new RegExp(' . Utils::escapeJavaScript($md_lookbehind . $value) . ', "giu"));'; $js[] = 'autolinker_regexes.set(' . Utils::escapeJavaScript('paste_' . $key) . ', new RegExp(' . Utils::escapeJavaScript('(?<=^|\s|
    )' . $value . '(?=$|\s|
    |\p{Po})') . ', "giu"));'; - $js[] = 'autolinker_regexes.set(' . Utils::escapeJavaScript('keypress_' . $key) . ', new RegExp(' . Utils::escapeJavaScript($value . '(?=[\p{Po}' . preg_quote(implode('', array_merge(array_keys(self::$balanced_pairs), self::$balanced_pairs)), '/') . ']*\s$)') . ', "giu"));'; + $js[] = 'autolinker_regexes.set(' . Utils::escapeJavaScript('keypress_' . $key) . ', new RegExp(' . Utils::escapeJavaScript($md_lookbehind . $value . '(?=[\p{Po}' . preg_quote(implode('', array_merge(array_keys(self::$balanced_pairs), self::$balanced_pairs)), '/') . ']*\s$)') . ', "giu"));'; } $js[] = 'const autolinker_balanced_pairs = new Map();'; 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 c23d1c60b3..e6c3a3784a 100644 --- a/Sources/Draft.php +++ b/Sources/Draft.php @@ -505,7 +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']); + $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/Editor.php b/Sources/Editor.php index 13aa836183..c521fa224b 100644 --- a/Sources/Editor.php +++ b/Sources/Editor.php @@ -216,8 +216,6 @@ public function __construct(array $options) $this->id = (string) ($options['id'] ?? 'message'); $this->value = strtr((string) ($options['value'] ?? ''), [ - // Tabs are not shown in SCEditor; replace with spaces. - "\t" => ' ', // The [#] item code for creating list items causes issues with // SCEditor, but [+] is a safe equivalent. '[#]' => '[+]', @@ -588,6 +586,11 @@ protected function buildBbcToolbar(): void 'code' => 'code', 'description' => Lang::$editortxt['code'], ], + [ + 'image' => 'tt', + 'code' => 'tt', + 'description' => Lang::$editortxt['tt'], + ], [ 'code' => 'quote', 'description' => Lang::$editortxt['insert_quote'], @@ -605,6 +608,11 @@ protected function buildBbcToolbar(): void 'code' => 'horizontalrule', 'description' => Lang::$editortxt['insert_horizontal_rule'], ], + [ + 'image' => 'heading', + 'code' => 'heading', + 'description' => Lang::$editortxt['heading'], + ], [], [ 'code' => 'maximize', diff --git a/Sources/Mail.php b/Sources/Mail.php index c0c57b1c07..27e31a0037 100644 --- a/Sources/Mail.php +++ b/Sources/Mail.php @@ -117,6 +117,9 @@ public static function send( $message = preg_replace('~(' . preg_quote(Config::$scripturl, '~') . '(?:[?/][\w\-_%\.,\?&;=#]+)?)~', '$1', $message); } + // Use real tabs. + $message = strtr($message, [Utils::TAB_SUBSTITUTE => $send_html ? '' . "\t" . '' : "\t"]); + list(, $from_name) = self::mimespecialchars(addcslashes($from !== null ? $from : Utils::$context['forum_name'], '<>()\'\\"'), true, $hotmail_fix, $line_break); list(, $subject) = self::mimespecialchars($subject, true, $hotmail_fix, $line_break); 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 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'] . ''; @@ -583,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)); @@ -872,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 e97ec992f2..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'] = BBCodeParser::load()->parse(preg_replace('~\[[/]?html\]~i', '', Utils::htmlspecialchars($package['description']))); + $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..7f779c8549 --- /dev/null +++ b/Sources/Parser.php @@ -0,0 +1,762 @@ + 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 properties + *******************/ + + /** + * @var array + * + * If not empty, only these BBCode tags will be parsed. + */ + public array $parse_tags = []; + + /** + * @var array + * + * List of disabled BBCode tags. + */ + public array $disabled = []; + + /** + * @var bool + * + * Enables special handling if output is meant for paper printing. + */ + public bool $for_print = false; + + /************************** + * 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 methods.
    +	 *******************/
    +
    +	/**
    +	 * Checks whether the server's load average is too high to parse BBCode.
    +	 *
    +	 * @return bool Whether the load average is too high.
    +	 */
    +	protected function highLoadAverage(): bool
    +	{
    +		return !empty(Utils::$context['load_average']) && !empty(Config::$modSettings['bbc']) && Utils::$context['load_average'] >= Config::$modSettings['bbc'];
    +	}
    +
    +	/**
    +	 * Sets $this->disabled.
    +	 */
    +	protected function setDisabled(): void
    +	{
    +		$this->disabled = [];
    +
    +		if (!empty(Config::$modSettings['disabledBBC'])) {
    +			$temp = explode(',', strtolower(Config::$modSettings['disabledBBC']));
    +
    +			foreach ($temp as $tag) {
    +				$this->disabled[trim($tag)] = true;
    +			}
    +
    +			if (in_array('color', $this->disabled)) {
    +				$this->disabled = array_merge(
    +					$this->disabled,
    +					[
    +						'black' => true,
    +						'white' => true,
    +						'red' => true,
    +						'green' => true,
    +						'blue' => true,
    +					],
    +				);
    +			}
    +		}
    +
    +		if (!empty($this->parse_tags)) {
    +			if (!in_array('email', $this->parse_tags)) {
    +				$this->disabled['email'] = true;
    +			}
    +
    +			if (!in_array('url', $this->parse_tags)) {
    +				$this->disabled['url'] = true;
    +			}
    +
    +			if (!in_array('iurl', $this->parse_tags)) {
    +				$this->disabled['iurl'] = true;
    +			}
    +		}
    +
    +		if ($this->for_print) {
    +			// [glow], [shadow], and [move] can't really be printed.
    +			$this->disabled['glow'] = true;
    +			$this->disabled['shadow'] = true;
    +			$this->disabled['move'] = true;
    +
    +			// Colors can't well be displayed... supposed to be black and white.
    +			$this->disabled['color'] = true;
    +			$this->disabled['black'] = true;
    +			$this->disabled['blue'] = true;
    +			$this->disabled['white'] = true;
    +			$this->disabled['red'] = true;
    +			$this->disabled['green'] = true;
    +			$this->disabled['me'] = true;
    +
    +			// Color coding doesn't make sense.
    +			$this->disabled['php'] = true;
    +
    +			// Links are useless on paper... just show the link.
    +			$this->disabled['ftp'] = true;
    +			$this->disabled['url'] = true;
    +			$this->disabled['iurl'] = true;
    +			$this->disabled['email'] = true;
    +			$this->disabled['flash'] = true;
    +
    +			// @todo Change maybe?
    +			if (!isset($_GET['images'])) {
    +				$this->disabled['img'] = true;
    +				$this->disabled['attach'] = true;
    +			}
    +
    +			// Maybe some custom BBC need to be disabled for printing.
    +			IntegrationHook::call('integrate_bbc_print', [&$this->disabled]);
    +		}
    +	}
    +
    +	/**
    +	 * Adjusts a BBCode definition so that it outputs its disabled version.
    +	 *
    +	 * @param array $code A BBCode definition.
    +	 * @return array The disabled version of the BBCode definition.
    +	 */
    +	protected function disableCode(array $code): array
    +	{
    +		if (!isset($code['disabled_before']) && !isset($code['disabled_after']) && !isset($code['disabled_content'])) {
    +			$code['before'] = !empty($code['block_level']) ? '
    ' : ''; + $code['after'] = !empty($code['block_level']) ? '
    ' : ''; + $code['content'] = isset($code['type']) && $code['type'] == 'closed' ? '' : (!empty($code['block_level']) ? '
    $1
    ' : '$1'); + } elseif (isset($code['disabled_before']) || isset($code['disabled_after'])) { + $code['before'] = $code['disabled_before'] ?? (!empty($code['block_level']) ? '
    ' : ''); + $code['after'] = $code['disabled_after'] ?? (!empty($code['block_level']) ? '
    ' : ''); + } else { + $code['content'] = $code['disabled_content']; + } + + return $code; + } + + /************************* + * 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(self::OUTPUT_HTML)->parse($string, true, $options); + } + + // 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(self::OUTPUT_BBC)->parse($string, false, $options); + $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, $options); + } + + 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 82% rename from Sources/BBCodeParser.php rename to Sources/Parsers/BBCodeParser.php index 397f3df00f..e8136c8d77 100644 --- a/Sources/BBCodeParser.php +++ b/Sources/Parsers/BBCodeParser.php @@ -13,139 +13,26 @@ 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 - *******************/ - - /** - * @var array - * - * If not empty, only these BBCode tags will be parsed. - */ - 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 - * - * List of disabled BBCode tags. - */ - 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 - * - * Enables special handling if output is meant for paper printing. - */ - public bool $for_print = false; - /********************* * Internal properties *********************/ @@ -157,35 +44,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 - * - * Holds any extra info that should be used in the cache_key. - * - * Data can be added to this variable using the integrate_pre_parsebbc hook. - * - * This is important if your mod can cause the same input string to produce - * different output strings in different situations. For example, if your - * mod adds a BBCode that shows different output to guests vs. members, then - * you need to add information to this variable in order to distinguish the - * guest version vs. the member version of the output. - */ - private array $cache_key_extras = []; - /** * @var array * @@ -272,13 +130,6 @@ class BBCodeParser */ private string $placeholder_template = "\u{E03C}" . '%1$s' . "\u{E03E}"; - /** - * @var array - * - * Holds parsed messages. - */ - private array $results = []; - /**************************** * Internal static properties ****************************/ @@ -416,8 +267,8 @@ class BBCodeParser ], [ 'tag' => 'b', - 'before' => '', - 'after' => '', + 'before' => '', + 'after' => '', ], // Legacy (equivalent to [ltr] or [rtl]) [ @@ -443,7 +294,8 @@ class BBCodeParser [ 'tag' => 'br', 'type' => 'closed', - 'content' => '
    ', + // We put a class on this to force the Markdown parser to preserve it. + 'content' => '
    ', ], [ 'tag' => 'center', @@ -542,10 +394,49 @@ class BBCodeParser 'before' => '', 'after' => '', ], + // For the h1-h6 tags, the element name will often change in the final + // output, but the class will not. For example, `

    ` + // might become `

    ` in the final output. + [ + 'tag' => 'h1', + 'before' => '

    ', + 'after' => '

    ', + 'block_level' => true, + ], + [ + 'tag' => 'h2', + 'before' => '

    ', + 'after' => '

    ', + 'block_level' => true, + ], + [ + 'tag' => 'h3', + 'before' => '

    ', + 'after' => '

    ', + 'block_level' => true, + ], + [ + 'tag' => 'h4', + 'before' => '

    ', + 'after' => '

    ', + 'block_level' => true, + ], + [ + 'tag' => 'h5', + 'before' => '
    ', + 'after' => '
    ', + 'block_level' => true, + ], + [ + 'tag' => 'h6', + 'before' => '
    ', + 'after' => '
    ', + 'block_level' => true, + ], [ 'tag' => 'html', 'type' => 'unparsed_content', - 'content' => '
    $1
    ', + 'content' => '
    $1
    ', 'block_level' => true, 'disabled_content' => '$1', ], @@ -557,8 +448,8 @@ class BBCodeParser ], [ 'tag' => 'i', - 'before' => '', - 'after' => '', + 'before' => '', + 'after' => '', ], [ 'tag' => 'img', @@ -622,7 +513,7 @@ class BBCodeParser [ 'tag' => 'list', 'parameters' => [ - 'type' => ['match' => '(none|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|upper-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha)'], + 'type' => ['match' => '(none|disc|circle|square)'], ], 'before' => '', @@ -630,6 +521,17 @@ class BBCodeParser 'require_children' => ['li'], 'block_level' => true, ], + [ + 'tag' => 'list', + 'parameters' => [ + 'type' => ['match' => '(decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|upper-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha)'], + ], + 'before' => '
      ', + 'after' => '
    ', + 'trim' => 'inside', + 'require_children' => ['li'], + 'block_level' => true, + ], [ 'tag' => 'ltr', 'before' => '', @@ -674,7 +576,7 @@ class BBCodeParser [ 'tag' => 'php', 'type' => 'unparsed_content', - 'content' => '$1', + 'content' => '$1', 'validate' => __CLASS__ . '::phpValidate', 'block_level' => false, 'disabled_content' => '$1', @@ -826,11 +728,10 @@ class BBCodeParser 'disabled_before' => '', 'disabled_after' => '', ], - // Legacy (the element is dead) [ 'tag' => 'tt', - 'before' => '', - 'after' => '', + 'before' => '', + 'after' => '', ], [ 'tag' => 'u', @@ -892,11 +793,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. @@ -904,32 +805,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->utf8 = $this->encoding === 'UTF-8'; - } + $this->for_print = $for_print; - 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(); @@ -937,27 +818,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 @@ -966,7 +832,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) === '') { @@ -977,7 +843,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(); @@ -991,172 +856,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; } /** @@ -1206,38 +915,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(); @@ -1975,22 +1652,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 { @@ -2000,9 +1677,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 { @@ -2014,7 +1692,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 = []; @@ -2027,152 +1705,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.
     	 */
    @@ -2597,7 +2137,7 @@ protected function parseMessage(): void
     				// Restore any placeholders
     				$data = strtr($data, $this->placeholders);
     
    -				$data = strtr($data, ["\t" => '   ']);
    +				$data = strtr($data, ["\t" => Utils::TAB_SUBSTITUTE]);
     
     				// If it wasn't changed, no copying or other boring stuff has to happen!
     				if ($data != substr($this->message, $this->last_pos, $this->pos - $this->last_pos)) {
    @@ -2656,7 +2196,7 @@ protected function parseMessage(): void
     
     			// Is this tag disabled?
     			if (isset($this->disabled[$tag['tag']])) {
    -				$tag = $this->useDisabledTag($tag);
    +				$tag = $this->disableCode($tag);
     			}
     
     			// The only special case is 'html', which doesn't need to close things.
    @@ -2675,20 +2215,15 @@ 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 = strtr($this->message, ["\n" => '']);
     
    -			$this->message = implode('', $message_parts);
    -		}
    -		// No smileys, just get rid of the markers.
    -		else {
    -			$this->message = strtr($this->message, ["\n" => '']);
    -		}
    +		// Transform the first table row into a table header and wrap the rest
    +		// in table body tags.
    +		$this->message = preg_replace_callback(
    +			'/(\X*?)<\/tr>(\X*?)<\/table>/u',
    +			fn ($matches) => '
    ' . preg_replace('~()~', '$1th$2', $matches[1]) . '' . $matches[2] . '
    ', + $this->message, + ); if ($this->message !== '' && $this->message[0] === ' ') { $this->message = ' ' . substr($this->message, 1); @@ -2698,101 +2233,13 @@ protected function parseMessage(): void $this->message = strtr($this->message, [' ' => '  ', "\r" => '', "\n" => '
    ', '
    ' => '
     ', ' ' => "\n"]); } - /** - * Checks whether the server's load average is too high to parse BBCode. - * - * @return bool Whether the load average is too high. - */ - protected function highLoadAverage(): bool - { - return !empty(Utils::$context['load_average']) && !empty(Config::$modSettings['bbc']) && Utils::$context['load_average'] >= Config::$modSettings['bbc']; - } - - /** - * Sets $this->disabled. - */ - protected function setDisabled(): void - { - $this->disabled = []; - - if (!empty(Config::$modSettings['disabledBBC'])) { - $temp = explode(',', strtolower(Config::$modSettings['disabledBBC'])); - - foreach ($temp as $tag) { - $this->disabled[trim($tag)] = true; - } - - if (in_array('color', $this->disabled)) { - $this->disabled = array_merge( - $this->disabled, - [ - 'black' => true, - 'white' => true, - 'red' => true, - 'green' => true, - 'blue' => true, - ], - ); - } - } - - if (!empty($this->parse_tags)) { - if (!in_array('email', $this->parse_tags)) { - $this->disabled['email'] = true; - } - - if (!in_array('url', $this->parse_tags)) { - $this->disabled['url'] = true; - } - - if (!in_array('iurl', $this->parse_tags)) { - $this->disabled['iurl'] = true; - } - } - - if ($this->for_print) { - // [glow], [shadow], and [move] can't really be printed. - $this->disabled['glow'] = true; - $this->disabled['shadow'] = true; - $this->disabled['move'] = true; - - // Colors can't well be displayed... supposed to be black and white. - $this->disabled['color'] = true; - $this->disabled['black'] = true; - $this->disabled['blue'] = true; - $this->disabled['white'] = true; - $this->disabled['red'] = true; - $this->disabled['green'] = true; - $this->disabled['me'] = true; - - // Color coding doesn't make sense. - $this->disabled['php'] = true; - - // Links are useless on paper... just show the link. - $this->disabled['ftp'] = true; - $this->disabled['url'] = true; - $this->disabled['iurl'] = true; - $this->disabled['email'] = true; - $this->disabled['flash'] = true; - - // @todo Change maybe? - if (!isset($_GET['images'])) { - $this->disabled['img'] = true; - $this->disabled['attach'] = true; - } - - // Maybe some custom BBC need to be disabled for printing. - IntegrationHook::call('integrate_bbc_print', [&$this->disabled]); - } - } - /** * Sets $this->bbc_codes. */ 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]; @@ -2947,11 +2394,25 @@ 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; } - $data = preg_replace('~<a\s+href=((?:")?)((?:https?://|ftps?://|mailto:|tel:)\S+?)\1>(.*?)</a>~i', '[url="$2"]$3[/url]', $data); + $data = preg_replace_callback( + '~<a\b\X+?href=((?:"|")?)(\X*?)\1\X*?>(\X*?)</a>~ui', + function ($matches) { + if ([$matches[2]] !== Autolinker::load()->detectUrls($matches[2])) { + return $matches[0]; + } + + if (str_starts_with($matches[2], Config::$boardurl)) { + return self::$enable_bbc ? '[iurl="' . $matches[2] . '"]' . $matches[3] . '[/iurl]' : '' . $matches[3] . ''; + } + + return self::$enable_bbc ? '[url="' . $matches[2] . '"]' . $matches[3] . '[/url]' : '' . $matches[3] . ''; + }, + $data, + ); //
    should be empty. $empty_tags = ['br', 'hr']; @@ -3378,28 +2839,6 @@ protected function parseItemCode(): void } } - /** - * Adjusts a tag definition so that it uses its disabled version for output. - * - * @param array $tag A tag definition. - * @return array The disabled version of the tag definition. - */ - protected function useDisabledTag(array $tag): array - { - if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content'])) { - $tag['before'] = !empty($tag['block_level']) ? '
    ' : ''; - $tag['after'] = !empty($tag['block_level']) ? '
    ' : ''; - $tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '
    $1
    ' : '$1'); - } elseif (isset($tag['disabled_before']) || isset($tag['disabled_after'])) { - $tag['before'] = $tag['disabled_before'] ?? (!empty($tag['block_level']) ? '
    ' : ''); - $tag['after'] = $tag['disabled_after'] ?? (!empty($tag['block_level']) ? '
    ' : ''); - } else { - $tag['content'] = $tag['disabled_content']; - } - - return $tag; - } - /** * Similar to $this->closeTags(), but only for inline tags. * Operates directly on $this->message. @@ -3472,7 +2911,7 @@ protected function transformToHtml(array $tag, array $params): void $this->open_tags[] = $tag; // There's no data to change, but maybe do something based on params? - $data = null; + $data = []; if (isset($tag['validate'])) { call_user_func_array($tag['validate'], [&$tag, &$data, $this->disabled, $params]); @@ -4151,7 +3590,6 @@ protected function resetRuntimeProperties(): void $to_reset = [ 'message', 'bbc_codes', - 'smileys', 'parse_tags', 'open_tags', 'inside', @@ -4159,12 +3597,12 @@ protected function resetRuntimeProperties(): void 'last_pos', 'placeholders', 'placeholders_counter', - 'cache_key_extras', ]; $class_vars = get_class_vars(__CLASS__); foreach ($to_reset as $var) { + unset($this->{$var}); $this->{$var} = $class_vars[$var]; } } @@ -4192,7 +3630,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/Parsers/MarkdownParser.php b/Sources/Parsers/MarkdownParser.php new file mode 100644 index 0000000000..4532e8d542 --- /dev/null +++ b/Sources/Parsers/MarkdownParser.php @@ -0,0 +1,4118 @@ +output_type. + * + * Used to set the output to HTML rendered the same way that the reference + * implementation of CommonMark would. + */ + public const OUTPUT_HTML_STRICT = 3; + + /** + * @var int + * + * Possible value for $this->hard_breaks. + * + * Using this option, line breaks will be converted to
    elements when + * the line breaks create blank lines. This can be used to preserve blank + * lines in the input while still parsing paragraph content normally. + */ + public const BR_LINES = 0b01; + + /** + * @var int + * + * Possible value for $this->hard_breaks. + * + * Using this option, line breaks will be converted to
    elements inside + * paragraphs, etc. + */ + public const BR_IN_PARAGRAPHS = 0b10; + + /** + * @var array + * + * Characters that can be escaped with a backslash. + */ + public const ESCAPEABLE = [ + '!', '"', '#', '$', '%', '&', '\'', '\\', + '(', ')', '*', '+', ',', '-', '.', '/', + ':', ';', '<', '=', '>', '?', '@', '|', + '[', ']', '^', '_', '`', '{', '}', '~', + ]; + + /** + * @var string + * + * Regex to match HTML tags, including opening tags, closing tags, comments, + * processing instructions, declarations, and CDATA sections. Matches both + * standard HTML5 tag names and custom tag names. + */ + public const REGEX_HTML_TAG = + '(' . + '(?P>opening_tag)' . + '|' . + '(?P>closing_tag)' . + '|' . + '(?P>comment)' . + '|' . + '(?P>processing_instruction)' . + '|' . + '(?P>declaration)' . + '|' . + '(?P>cdata)' . + ')' . + '(?(DEFINE)' . + '(?[a-zA-Z][a-zA-Z0-9\-]*)' . + '(?[^\s"\'=<>`])' . + '(?\'[^\']*\')' . + '(?"[^"]*")' . + '(?(?P>attribute_value_unquoted)|(?P>attribute_value_single_quoted)|(?P>attribute_value_double_quoted))' . + '(?\s*=\s*(?P>attribute_value))' . + '(?[a-zA-Z_:][a-zA-Z0-9_.:\-]*)' . + '(?\s+(?P>attribute_name)(?P>attribute_value_specification)?)' . + '(?<(?P>tag_name)(?P>attribute)*\s*/?>)' . + '(?tag_name)\s*>)' . + '(?)' . + '(?<' . '\?\X*?\?' . '>)' . + '(?]+>)' . + '(?)' . + ')'; + + /** + * @var string + * + * Regular expression to match link text. + */ + public const REGEX_LINK_TEXT = + '(?P' . + // Opening bracket. + '\[' . + // Any number of... + '(?' . '>' . + // characters that are... + '(?' . '>' . + // not square brackets... + '[^\[\]]' . + // or + '|' . + // escaped square brackets... + '\\\\[\[\]]' . + ')' . + // or + '|' . + // balanced square brackets. + '(?P>text)' . + ')*' . + // Closing bracket. + '\]' . + ')'; + + /** + * @var string + * + * Regular expression to match link labels. + */ + public const REGEX_LINK_LABEL = + '(?P