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('~<span class="remove">(.+?)</span>~', '$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 = ' ';
-
- $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|span)><(?: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('~(?)td(>)~', '$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*/?>)' .
+ '(?(?P>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' .
+ // Opening bracket.
+ '\[' .
+ // Must contain at least one non-whitespace character.
+ '(?=\s*\S)' .
+ // Any number of other characters or escaped closing brackets.
+ '(?:' .
+ '\X(?!\])' .
+ '|' .
+ '\\\\(?=\])' .
+ ')*' .
+ '\X?' .
+ // Closing bracket.
+ '\]' .
+ ')';
+
+ /**
+ * @var string
+ *
+ * Regular expression to match link destinations.
+ */
+ public const REGEX_LINK_DESTINATION =
+ '(?P' .
+ '(?:' .
+ // Angle brackets that contain no vertical whitespace.
+ '<[^\v>]*>' .
+ // or
+ '|' .
+ // Non-space, non-control characters.
+ '[^\s\p{Cc}]+' .
+ ')' .
+ ')';
+
+ /**
+ * @var string
+ *
+ * Regular expression to match link titles.
+ */
+ public const REGEX_LINK_TITLE =
+ '(?P' .
+ // Opening quotation mark.
+ '(?P["\'])' .
+ // Any number of other characters or escaped quotation marks.
+ '(?:' .
+ '\X(?!(?P>quote))' .
+ '|' .
+ '\\\\(?=(?P>quote))' .
+ ')*' .
+ '\X?' .
+ // Closing quotation mark.
+ '(?P>quote)' .
+
+ // or
+ '|' .
+
+ // Opening parenthesis.
+ '\(' .
+ // Any number of other characters or escaped closing parentheses.
+ '(?:' .
+ '\X(?!\))' .
+ '|' .
+ '\\\\(?=\))' .
+ ')*' .
+ '\X?' .
+ // Closing parenthesis.
+ '\)' .
+ ')';
+
+ /**
+ * @var string
+ *
+ * Regular expression to match link reference definitions.
+ */
+ public const REGEX_LINK_REF_DEF =
+ // Max indentation of three spaces.
+ '^\h{0,3}' .
+ // Link label.
+ self::REGEX_LINK_LABEL .
+ // Colon.
+ ':' .
+ // Optional whitespace, with up to one line ending.
+ '\h*\n?\h*' .
+ // Link destination.
+ self::REGEX_LINK_DESTINATION .
+ // Optional link title.
+ '(?:' .
+ // Whitespace, with up to one line ending.
+ '(?:\h+\n?|\n)\h*' .
+ // Link title itself.
+ self::REGEX_LINK_TITLE .
+ ')?' .
+ // Trailing whitespace and end of line.
+ '\h*(?:\n|$)';
+
+ /**
+ * @var string
+ *
+ * Regular expression to match link reference definitions.
+ */
+ public const REGEX_LINK_INLINE =
+ // Link text.
+ self::REGEX_LINK_TEXT .
+ // Opening parenthesis.
+ '\(' .
+ // Optional whitespace.
+ '\s*' .
+ // Optional link destination.
+ '(?:' .
+ self::REGEX_LINK_DESTINATION .
+ ')?' .
+ // Optional link title.
+ '(?:' .
+ '\s+' .
+ self::REGEX_LINK_TITLE .
+ ')?' .
+ // Optional whitespace.
+ '\s*' .
+ // Closing parenthesis.
+ '\)';
+
+ /**
+ * @var string
+ *
+ * Regular expression to match full link references.
+ */
+ public const REGEX_LINK_REF_FULL = self::REGEX_LINK_TEXT . self::REGEX_LINK_LABEL;
+
+ /**
+ * @var string
+ *
+ * Regular expression to match collapsed link references.
+ */
+ public const REGEX_LINK_REF_COLLAPSED = self::REGEX_LINK_LABEL . '\[\]';
+
+ /**
+ * @var string
+ *
+ * Regular expression to match shortcut link references.
+ */
+ public const REGEX_LINK_REF_SHORTCUT = self::REGEX_LINK_LABEL . '(?!\[)';
+
+ /*******************
+ * Public properties
+ *******************/
+
+ /**
+ * @var int
+ *
+ * The type of output to generate.
+ *
+ * Value must be one of this class's OUTPUT_* constants.
+ */
+ public int $output_type = self::OUTPUT_HTML;
+
+ /**
+ * @var int
+ *
+ * How to render line breaks.
+ *
+ * Value should be a bitmask of this class's BR_* constants.
+ */
+ public int $hard_breaks = 0;
+
+ /*********************
+ * Internal properties
+ *********************/
+
+ /**
+ * @var array
+ *
+ * Defines all the recognized block level element types.
+ *
+ * The order of items in this array matters. Don't change it.
+ */
+ protected array $block_types = [
+ 'blank' => [
+ 'is_container' => false,
+ 'interrupts_p' => true,
+ 'marker_pattern' => false,
+ 'opener_test' => 'testIsBlank',
+ 'continue_test' => 'testIsBlank',
+ 'closer_test' => '!testIsBlank',
+ 'add' => null,
+ 'append' => 'appendBlank',
+ 'close' => null,
+ ],
+ 'root' => [
+ 'is_container' => true,
+ 'interrupts_p' => false,
+ 'marker_pattern' => false,
+ 'opener_test' => false,
+ 'continue_test' => false,
+ 'closer_test' => false,
+ 'add' => null,
+ 'append' => null,
+ 'close' => null,
+ ],
+ 'fenced_code' => [
+ 'is_container' => false,
+ 'interrupts_p' => true,
+ 'marker_pattern' => '/^([`~]){3,}(?!\h+\1)/u',
+ 'opener_test' => 'testIsFencedCode',
+ 'continue_test' => true,
+ 'closer_test' => 'testIsCodeFence',
+ 'add' => 'addFencedCode',
+ 'append' => 'appendFencedCode',
+ 'close' => 'closeCodeBlock',
+ ],
+ 'blockquote' => [
+ 'is_container' => true,
+ 'interrupts_p' => true,
+ 'marker_pattern' => '/^>\h?/u',
+ 'opener_test' => 'testOpensQuote',
+ 'continue_test' => 'testOpensQuote',
+ 'closer_test' => 'testClosesQuote',
+ 'add' => 'addBlock',
+ 'append' => null,
+ 'close' => 'closeBlock',
+ ],
+ 'list' => [
+ 'is_container' => true,
+ 'interrupts_p' => true,
+ 'marker_pattern' => false,
+ 'opener_test' => false,
+ 'continue_test' => false,
+ 'closer_test' => false,
+ 'add' => null,
+ 'append' => null,
+ 'close' => null,
+ ],
+ 'list_item' => [
+ 'is_container' => true,
+ 'interrupts_p' => true,
+ 'marker_pattern' => '/^((?P[*+-])|(?P\d+)(?P[.)]))\h+/u',
+ 'opener_test' => 'testOpensListItem',
+ 'continue_test' => 'testContinuesListItem',
+ 'closer_test' => 'testClosesListItem',
+ 'add' => 'addListItem',
+ 'append' => null,
+ 'close' => 'closeBlock',
+ ],
+ 'indented_code' => [
+ 'is_container' => false,
+ 'interrupts_p' => false,
+ 'marker_pattern' => '/^\h{4}/u',
+ 'opener_test' => 'testIsIndentedCode',
+ 'continue_test' => 'testIsIndentedCode',
+ 'closer_test' => '!testIsIndentedCode',
+ 'add' => 'addIndentedCode',
+ 'append' => 'appendIndentedCode',
+ 'close' => 'closeCodeBlock',
+ ],
+ 'hr' => [
+ 'is_container' => false,
+ 'interrupts_p' => true,
+ 'marker_pattern' => '/^[*-_]/u',
+ 'opener_test' => 'testIsHr',
+ 'continue_test' => false,
+ 'closer_test' => true,
+ 'add' => 'addBlock',
+ 'append' => null,
+ 'close' => 'closeBlock',
+ ],
+ 'atx_heading' => [
+ 'is_container' => false,
+ 'interrupts_p' => true,
+ 'marker_pattern' => '/^#{1,6}/u',
+ 'opener_test' => 'testIsAtxHeading',
+ 'continue_test' => false,
+ 'closer_test' => true,
+ 'add' => 'addAtxHeading',
+ 'append' => null,
+ 'close' => 'closeBlock',
+ ],
+ 'setext_heading' => [
+ 'is_container' => false,
+ 'interrupts_p' => false,
+ 'marker_pattern' => '/^[=-]/u',
+ 'opener_test' => 'testIsSetextHeading',
+ 'continue_test' => false,
+ 'closer_test' => true,
+ 'add' => 'addSetextHeading',
+ 'append' => null,
+ 'close' => 'closeBlock',
+ ],
+ 'html_1' => [
+ 'is_container' => false,
+ 'interrupts_p' => true,
+ 'marker_pattern' => '/^<(script|pre|style)(?=\h|>|$)/ui',
+ 'opener_test' => 'testOpensHtml1',
+ 'continue_test' => '!testClosesHtml1',
+ 'closer_test' => 'testClosesHtml1',
+ 'add' => 'addBlock',
+ 'append' => 'appendContent',
+ 'close' => 'closeBlock',
+ ],
+ 'html_2' => [
+ 'is_container' => false,
+ 'interrupts_p' => true,
+ 'marker_pattern' => '/^');
+ break;
+
+ case 3:
+ $closes = str_contains($prev_line_info['content'], '?' . '>');
+ break;
+
+ case 4:
+ $closes = str_contains($prev_line_info['content'], '>');
+ break;
+
+ case 5:
+ $closes = str_contains($prev_line_info['content'], ']]>');
+ break;
+
+ case 6:
+ case 7:
+ $closes = $this->testIsBlank($line_info);
+ break;
+ }
+
+ if ($closes) {
+ $this->in_html = 0;
+ }
+
+ return (bool) $closes;
+ }
+
+ /**
+ * Tests whether a line is part of a table.
+ *
+ * @param array $line_info Info about the current line.
+ * @return bool Whether this line is part of a table.
+ */
+ protected function testIsTable(array $line_info): bool
+ {
+ if (in_array('table', $line_info['possible_types'])) {
+ return true;
+ }
+
+ if ($line_info['indent'] > 3) {
+ $this->table_align = [];
+
+ return false;
+ }
+
+ // Already in a table.
+ if ($this->table_align !== []) {
+ // Any other block element breaks the table.
+ if (!$this->testIsParagraph($line_info)) {
+ $this->table_align = [];
+ }
+
+ return $this->table_align !== [];
+ }
+
+ // At this point, we're checking if this is a new table.
+ $last_open = $this->open[array_key_last($this->open)];
+
+ // A valid header row will initially be parsed as a paragraph
+ if ($this->open[array_key_last($this->open)]['type'] !== 'p') {
+ return false;
+ }
+
+ // Must have exactly one header row.
+ if (count($this->open[array_key_last($this->open)]['content']) !== 1) {
+ return false;
+ }
+
+ $thead_cells = array_map('\SMF\Utils::htmlTrim', preg_split('/(? $delim_cell) {
+ if (!preg_match('/^(?P:?)-+(?P:?)$/u', $delim_cell, $matches)) {
+ return false;
+ }
+
+ $align = !empty($matches['left']) ? 'left' : null;
+ $align = !empty($matches['right']) ? (isset($align) ? 'center' : 'right') : $align;
+
+ $delim_cells[$d] = $align ?? 'none';
+ }
+
+ // The table delimiter line doesn't count if it was just '-----'.
+ // It must have either a '|' or a ':' in it somewhere.
+ if (
+ count($delim_cells) === 1
+ && $delim_cells[0] === 'none'
+ && !preg_match('/(?table_align = $delim_cells;
+
+ return true;
+ }
+
+ /**
+ * Tests whether a line can be part of a paragraph.
+ *
+ * @param array $line_info Info about the current line.
+ * @return bool Whether this line can be part of a paragraph.
+ */
+ protected function testIsParagraph(array $line_info): bool
+ {
+ foreach (array_diff(array_keys($this->block_types), $line_info['tested_types']) as $untested) {
+ $opener_test = $this->getMethod($def['opener_test'] ?? false);
+
+ if (is_callable($opener_test) && $opener_test($line_info)) {
+ return false;
+ }
+ }
+
+ return array_diff($line_info['possible_types'], ['p']) === [];
+ }
+
+ /*
+ * Part 1.b: Building block structure.
+ */
+
+ /**
+ * The default method for adding a new block element to the structure.
+ * This is used for any block type that doesn't have its own special method.
+ *
+ * @param array $line_info Info about the current line.
+ * @param int $last_container Key of the last container block in $this->open.
+ * @param int $o Key of the current block in $this->open.
+ */
+ protected function addBlock(array $line_info, int $last_container, int $o): void
+ {
+ while (
+ $last_container > 0
+ && $line_info['type'] !== 'blank'
+ && $this->block_types[$this->open[$last_container - 1]['type']]['is_container']
+ && $this->open[$last_container]['properties']['indent'] > $line_info['indent']
+ ) {
+ $last_container--;
+ }
+
+ $properties = $line_info['properties'] ?? [];
+
+ if (!isset($properties['indent'])) {
+ $properties['indent'] = $line_info['indent'];
+ }
+
+ $this->open[$last_container]['content'][] = [
+ 'type' => $line_info['type'],
+ 'open' => $this->block_types[$line_info['type']]['continue_test'] !== false,
+ 'properties' => $properties,
+ 'content' => $this->block_types[$line_info['type']]['is_container'] || !isset($line_info['content']) ? [] : [$line_info['content']],
+ ];
+ }
+
+ /**
+ * Adds a list item to the structure.
+ *
+ * If necessary, first adds a list to the structure to hold the list item.
+ *
+ * @param array $line_info Info about the current line.
+ * @param int $last_container Key of the last container block in $this->open.
+ * @param int $o Key of the current block in $this->open.
+ */
+ protected function addListItem(array $line_info, int $last_container, int $o): void
+ {
+ preg_match($this->block_types['list_item']['marker_pattern'], $line_info['content'], $matches);
+
+ // This should never happen, but just in case...
+ if (empty($matches)) {
+ $this->addBlock($line_info, $last_container, $o);
+
+ return;
+ }
+
+ $marker = $matches[0];
+ $bullet = ($matches['bullet'] ?? '') !== '' ? $matches['bullet'] : null;
+ $number = ($matches['number'] ?? '') !== '' ? $matches['number'] : null;
+ $num_punct = ($matches['num_punct'] ?? '') !== '' ? $matches['num_punct'] : null;
+
+ $indent = $line_info['indent'] + mb_strlen($marker) + strspn($line_info['content'], ' ', strlen($marker));
+
+ // If this list item doesn't match the existing list's type,
+ // exit the existing list so we can start a new one.
+ if (
+ $this->open[$last_container]['type'] === 'list'
+ && (
+ isset($number) !== $this->open[$last_container]['properties']['ordered']
+ || (
+ isset($bullet)
+ && $bullet !== $this->open[$last_container]['properties']['marker_char']
+ )
+ || (
+ isset($num_punct)
+ && $num_punct !== $this->open[$last_container]['properties']['marker_char']
+ )
+ )
+ ) {
+ do {
+ $last_container--;
+ } while ($last_container > 0 && !$this->block_types[$this->open[$last_container]['type']]['is_container']);
+ }
+
+ // Do we need to start a new list?
+ if ($this->open[$last_container]['type'] !== 'list') {
+ $this->open[$last_container]['content'][] = [
+ 'type' => 'list',
+ 'open' => true,
+ 'properties' => [
+ 'ordered' => isset($number),
+ 'start' => (int) ($number ?? 0),
+ 'marker_char' => $bullet ?? $num_punct ?? '',
+ 'indent' => $indent,
+ ],
+ 'content' => [
+ [
+ 'type' => 'list_item',
+ 'open' => true,
+ 'properties' => [
+ 'indent' => $indent,
+ ],
+ 'content' => [],
+ ],
+ ],
+ ];
+ }
+ // Just add an item to the existing list.
+ else {
+ $this->open[$last_container]['content'][] = [
+ 'type' => 'list_item',
+ 'open' => true,
+ 'properties' => [
+ 'indent' => $indent,
+ ],
+ 'content' => [],
+ ];
+ }
+ }
+
+ /**
+ * Adds an ATX heading to the structure.
+ *
+ * @param array $line_info Info about the current line.
+ * @param int $last_container Key of the last container block in $this->open.
+ * @param int $o Key of the current block in $this->open.
+ */
+ protected function addAtxHeading(array $line_info, int $last_container, int $o): void
+ {
+ $line_info['properties']['level'] = strspn($line_info['content'], '#');
+
+ $line_info['content'] = strtr(preg_replace('/^\h*#+\h*|(\h+#+)?\h*$/u', '', strtr($line_info['content'], ['\\#' => "\u{E000}"])), ["\u{E000}" => '\\#']);
+
+ $this->addBlock($line_info, $last_container, $o);
+ }
+
+ /**
+ * Adds a setext heading to the structure.
+ *
+ * @param array $line_info Info about the current line.
+ * @param int $last_container Key of the last container block in $this->open.
+ * @param int $o Key of the current block in $this->open.
+ */
+ protected function addSetextHeading(array $line_info, int $last_container, int $o): void
+ {
+ $this->open[$o]['type'] = 'setext_heading';
+ $this->open[$o]['properties']['level'] = str_starts_with($line_info['content'], '=') ? 1 : 2;
+ }
+
+ /**
+ * Adds an indented code block to the structure.
+ *
+ * @param array $line_info Info about the current line.
+ * @param int $last_container Key of the last container block in $this->open.
+ * @param int $o Key of the current block in $this->open.
+ */
+ protected function addIndentedCode(array $line_info, int $last_container, int $o): void
+ {
+ $indent = 4;
+
+ if (
+ isset($this->open[$last_container])
+ && $this->open[$last_container]['type'] !== 'indented_code'
+ ) {
+ $indent += $this->open[$last_container]['properties']['indent'] ?? 0;
+ }
+
+ $this->open[$last_container]['content'][] = [
+ 'type' => $line_info['type'],
+ 'open' => $this->block_types[$line_info['type']]['continue_test'] !== false,
+ 'properties' => [
+ 'indent' => $indent,
+ ],
+ 'content' => [
+ ($line_info['indent'] > $indent ? str_repeat(' ', $line_info['indent'] - $indent) : '') . $line_info['content'],
+ ],
+ ];
+ }
+
+ /**
+ * Adds a fenced code block to the structure.
+ *
+ * @param array $line_info Info about the current line.
+ * @param int $last_container Key of the last container block in $this->open.
+ * @param int $o Key of the current block in $this->open.
+ */
+ protected function addFencedCode(array $line_info, int $last_container, int $o): void
+ {
+ // Opening code fence.
+ if ($this->testIsCodeFence($line_info)) {
+ $line_info['properties']['indent'] = $line_info['indent'];
+ $line_info['properties']['info_string'] = $this->info_string;
+ unset($line_info['content']);
+
+ $this->addBlock($line_info, $last_container, $o);
+
+ return;
+ }
+
+ // Otherwise, adding is really just appending.
+ $this->appendContent($line_info, $last_container, $o);
+ }
+
+ /**
+ * Adds a table to the structure.
+ *
+ * @param array $line_info Info about the current line.
+ * @param int $last_container Key of the last container block in $this->open.
+ * @param int $o Key of the current block in $this->open.
+ */
+ protected function addTable(array $line_info, int $last_container, int $o): void
+ {
+ if ($this->open[$o]['type'] !== 'p' || count($this->open[$o]['content']) !== 1) {
+ return;
+ }
+
+ $cells = array_map('\SMF\Utils::htmlTrim', preg_split('/(?open[$o]['content'][0], -1, PREG_SPLIT_NO_EMPTY));
+
+ $tr = [];
+
+ foreach ($cells as $th) {
+ $tr[] = $th;
+ }
+
+ $this->open[$o]['type'] = 'table';
+ $this->open[$o]['properties']['align'] = $this->table_align;
+ $this->open[$o]['content'] = [$tr];
+ }
+
+ /**
+ * The default method for appending content to an existing block element.
+ * This is used for any block type that doesn't have its own special method.
+ *
+ * @param array $line_info Info about the current line.
+ * @param int $last_container Key of the last container block in $this->open.
+ * @param int $o Key of the current block in $this->open.
+ */
+ protected function appendContent(array $line_info, int $last_container, int $o): void
+ {
+ if (
+ !empty($this->open[$o]['properties']['blank_after'])
+ && (
+ $this->hard_breaks & self::BR_IN_PARAGRAPHS
+ || (
+ $this->hard_breaks & self::BR_LINES
+ && $this->open[$o]['properties']['blank_after'] > 1
+ )
+ )
+ ) {
+ for ($i = 0; $i < $this->open[$o]['properties']['blank_after']; $i++) {
+ $this->open[$o]['content'][] = "\n";
+ }
+ }
+
+ $this->open[$o]['properties']['blank_after'] = 0;
+ $this->open[$o]['content'][] = $line_info['content'];
+ }
+
+ /**
+ * Appends a blank line to the currently open block.
+ *
+ * Blank lines get special handling compared to other appended content.
+ *
+ * @param array $line_info Info about the current line.
+ * @param int $last_container Key of the last container block in $this->open.
+ * @param int $o Key of the current block in $this->open.
+ */
+ protected function appendBlank(array $line_info, int $last_container, int $o): void
+ {
+ if (!isset($this->last_block['properties'])) {
+ return;
+ }
+
+ if (!isset($this->last_block['properties']['blank_after'])) {
+ $this->last_block['properties']['blank_after'] = 0;
+ }
+
+ $this->last_block['properties']['blank_after']++;
+ }
+
+ /**
+ * Appends a line to a currently open fenced code block.
+ *
+ * @param array $line_info Info about the current line.
+ * @param int $last_container Key of the last container block in $this->open.
+ * @param int $o Key of the current block in $this->open.
+ */
+ protected function appendFencedCode(array $line_info, int $last_container, int $o): void
+ {
+ // If this line is the closing code fence, just turn off $this->in_code.
+ if ($this->testIsCodeFence($line_info)) {
+ $this->in_code = 0;
+
+ return;
+ }
+
+ $indent = $this->open[$o]['properties']['indent'] ?? 0;
+
+ $this->open[$o]['content'][] = ($line_info['indent'] > $indent ? str_repeat(' ', $line_info['indent'] - $indent) : '') . Utils::htmlTrimLeft($line_info['content']);
+ }
+
+ /**
+ * Appends a line to a currently open indented code block.
+ *
+ * @param array $line_info Info about the current line.
+ * @param int $last_container Key of the last container block in $this->open.
+ * @param int $o Key of the current block in $this->open.
+ */
+ protected function appendIndentedCode(array $line_info, int $last_container, int $o): void
+ {
+ $indent = $this->open[$o]['properties']['indent'] ?? 0;
+
+ $this->open[$o]['content'][] = ($line_info['indent'] > $indent ? str_repeat(' ', $line_info['indent'] - $indent) : '') . $line_info['content'];
+ }
+
+ /**
+ * Appends a line to a currently open table as a new table row.
+ *
+ * @param array &$line_info Info about the current line.
+ * @param int $last_container Key of the last container block in $this->open.
+ * @param int $o Key of the current block in $this->open.
+ */
+ protected function appendTableRow(array &$line_info, int $last_container, int $o): void
+ {
+ $cells = array_map('\SMF\Utils::htmlTrim', preg_split('/(?open[$o]['content'][] = $tr;
+ }
+
+ /**
+ * The default method for closing an open block element.
+ * This is used for any block type that doesn't have its own special method.
+ *
+ * @param int $o Key of the block in $this->open.
+ */
+ protected function closeBlock(int $o): void
+ {
+ $this->open[$o]['open'] = false;
+
+ // Closing a parent block automatically closes its children, too.
+ if ($o < array_key_last($this->open)) {
+ $this->closeBlock($o + 1);
+ }
+ }
+
+ /**
+ * Closes a code block.
+ *
+ * @param int $o Key of the block in $this->open.
+ */
+ protected function closeCodeBlock(int $o): void
+ {
+ // Remove unwanted blank lines from the end of code blocks.
+ $last_content = array_key_last($this->open[$o]['content']);
+
+ while (
+ isset($this->open[$o]['content'][$last_content])
+ && Utils::htmlTrim($this->open[$o]['content'][$last_content]) === ''
+ ) {
+ unset($this->open[$o]['content'][$last_content--]);
+ }
+
+ // Toggle the state of $this->in_code.
+ if ($this->in_code === ($this->open[$o]['type'] === 'fenced_code' ? 2 : 1)) {
+ $this->in_code = 0;
+ }
+
+ $this->closeBlock($o);
+ }
+
+ /**
+ * Closes an open table.
+ *
+ * @param int $o Key of the block in $this->open.
+ */
+ protected function closeTable(int $o): void
+ {
+ if ($this->table_align === []) {
+ $this->closeBlock($o);
+ }
+ }
+
+ /**
+ * Closes a paragraph.
+ *
+ * @param int $o Key of the block in $this->open.
+ */
+ protected function closeParagraph(int $o): void
+ {
+ // Check paragraphs for link reference definitions.
+ $content = implode("\n", $this->open[$o]['content']);
+
+ while (preg_match('~' . self::REGEX_LINK_REF_DEF . '~u', $content, $matches)) {
+ $text = $this->extractLinkLabel($matches['label']);
+ $url = $this->extractLinkUrl($matches['destination'] ?? '');
+ $title = $this->extractLinkTitle($matches['title'] ?? '');
+
+ // Title must not contain blank lines.
+ if (preg_match('/\n\h*\n/u', $title)) {
+ break;
+ }
+
+ // If a link reference for this label has not yet been set, do so.
+ if (!isset($this->link_reference_definitions[$text])) {
+ $this->link_reference_definitions[$text] = [
+ 'url' => $url,
+ 'title' => $title,
+ ];
+ }
+
+ // Remove the link reference definition.
+ $content = substr($content, strlen($matches[0]));
+ }
+
+ $this->open[$o]['content'] = array_filter(explode("\n", $content), 'strlen');
+
+ $this->closeBlock($o);
+ }
+
+ /*
+ * Part 2: Parsing inline structure.
+ */
+
+ /**
+ * Parses inline markup inside a block element and all of its children.
+ *
+ * @param array &$block A block element in which to parse inline content.
+ */
+ protected function parseInlines(array &$block): void
+ {
+ // Tables are special...
+ if ($block['type'] === 'table') {
+ foreach ($block['content'] as &$row) {
+ foreach ($row as &$cell) {
+ $cell = [
+ 'type' => 'td',
+ 'content' => [$cell],
+ ];
+
+ $this->parseInlines($cell);
+ }
+ }
+
+ return;
+ }
+
+ if (array_filter($block['content'], 'is_string') === []) {
+ // Recurse down to the leaf nodes.
+ foreach ($block['content'] as &$value) {
+ $this->parseInlines($value);
+ }
+ } elseif (in_array($block['type'], ['atx_heading', 'setext_heading', 'p', 'td'])) {
+ $content = $this->parseInlineFirstPass($block['content']);
+ $content = $this->parseInlineSecondPass($content);
+
+ $block['content'] = $content;
+ }
+ }
+
+ /**
+ * Parses inline code spans, HTML tags, and `` links.
+ *
+ * @param array $content The content of a paragraph or heading block.
+ */
+ protected function parseInlineFirstPass(array $content): array
+ {
+ $new_content = [];
+
+ $string = implode("\n", $content);
+ $chars = mb_str_split($string);
+
+ $escaped = false;
+ $in_code = false;
+ $code_delim = '';
+ $string_part = '';
+
+ for ($i = 0; $i < count($chars); $i++) {
+ $char = $chars[$i];
+
+ switch ($char) {
+ case '\\':
+ $escaped = true;
+ break;
+
+ // Starting or ending an inline code span.
+ case '`':
+ if ($escaped) {
+ $string_part .= $char;
+ $escaped = false;
+ break;
+ }
+
+ if (!$in_code) {
+ $code_delim .= '`';
+
+ while (isset($chars[$i + 1]) && $chars[$i + 1] === '`') {
+ $code_delim .= '`';
+ $i++;
+ }
+
+ $in_code = true;
+ $new_content[] = $string_part;
+ $string_part = '';
+ } else {
+ $temp = '`';
+ $temp_i = $i;
+
+ while (isset($chars[$temp_i + 1]) && $chars[++$temp_i] === '`') {
+ $temp .= '`';
+ }
+
+ if ($temp === $code_delim) {
+ $i = $temp_i - 1;
+ $in_code = false;
+
+ if ($string_part !== '') {
+ $new_content[] = [
+ 'type' => 'code',
+ 'properties' => [],
+ 'content' => [$string_part],
+ ];
+ }
+
+ $string_part = '';
+ }
+ }
+ break;
+
+ // Possibly starting an inline "autolink" or HTML tag.
+ case '<':
+ if ($escaped) {
+ $string_part .= $char;
+ $escaped = false;
+ break;
+ }
+
+ $new_content[] = $string_part;
+ $string_part = '';
+
+ $temp_string = '';
+
+ while (isset($chars[$i + 1]) && $chars[$i + 1] !== '>') {
+ $temp_string .= $chars[++$i];
+ }
+
+ // Is this a URI?
+ $urls = Autolinker::load()->detectUrls($temp_string);
+
+ if ($urls === [$temp_string]) {
+ $new_content[] = [
+ 'type' => 'link',
+ 'properties' => [
+ 'url' => $temp_string,
+ ],
+ 'content' => [$temp_string],
+ ];
+
+ $i++;
+
+ break;
+ }
+
+ // Is this an HTML tag?
+ if (preg_match('~' . self::REGEX_HTML_TAG . '~u', '<' . $temp_string . '>')) {
+ $new_content[] = [
+ 'type' => 'html',
+ 'properties' => [],
+ 'content' => ['<' . $temp_string . '>'],
+ ];
+
+ $i++;
+
+ break;
+ }
+
+ // If we get here, it's just a regular string.
+ $string_part .= '<' . $temp_string;
+ break;
+
+ default:
+ if ($escaped) {
+ if (!in_array($char, self::ESCAPEABLE)) {
+ $string_part .= '\\';
+ }
+
+ $escaped = false;
+ }
+
+ $string_part .= $char;
+ break;
+ }
+ }
+
+ if ($string_part !== '') {
+ $new_content[] = $string_part;
+ }
+
+ // Amalgamate contiguous plain strings.
+ $this->amalgamateStrings($new_content);
+
+ // Filter out empty strings and return.
+ return array_values(array_filter($new_content, fn ($arg) => $arg !== ''));
+ }
+
+ /**
+ * Parses all other types of inline Markdown syntax.
+ *
+ * @param array $content The content of a paragraph or heading block.
+ */
+ protected function parseInlineSecondPass(array $content): array
+ {
+ $new_content = [];
+
+ $last_c = array_key_last($content);
+
+ foreach ($content as $c => $string) {
+ if (!is_string($string)) {
+ $new_content[] = $string;
+ continue;
+ }
+
+ $chars = mb_str_split($string);
+ $last_i = array_key_last($chars);
+
+ $escaped = false;
+ $string_part = '';
+
+ for ($i = 0; $i < count($chars); $i++) {
+ $char = $chars[$i];
+
+ switch ($char) {
+ case '\\':
+ $escaped = true;
+ break;
+
+ case "\n":
+ // Don't create hard line breaks at the end of a block.
+ if ($c === $last_c && $i === $last_i) {
+ $string_part .= $char;
+ }
+ // Hard line break via an escaped newline.
+ elseif ($escaped) {
+ if ($string_part !== '') {
+ $new_content[] = $string_part;
+ $string_part = '';
+ }
+
+ $new_content[] = [
+ 'type' => 'html',
+ 'properties' => [],
+ 'content' => [' '],
+ ];
+
+ $escaped = false;
+
+ while (isset($chars[$i + 1]) && $chars[$i + 1] === ' ') {
+ $i++;
+ }
+ }
+ // Hard line break via two or more spaces and then a newline.
+ elseif ($i > 2 && $chars[$i - 1] === ' ' && $chars[$i - 2] === ' ') {
+ if ($string_part !== '') {
+ $new_content[] = Utils::htmlTrimRight($string_part);
+ $string_part = '';
+ }
+
+ $new_content[] = [
+ 'type' => 'html',
+ 'properties' => [],
+ 'content' => [' '],
+ ];
+
+ while (isset($chars[$i + 1]) && $chars[$i + 1] === ' ') {
+ $i++;
+ }
+ }
+ // Nothing special.
+ else {
+ $string_part .= $char;
+ }
+ break;
+
+ case '*':
+ case '_':
+ case '~':
+ if ($escaped) {
+ $string_part .= $char;
+ $escaped = false;
+ break;
+ }
+
+ if ($string_part !== '') {
+ $new_content[] = $string_part;
+ }
+
+ $string_part = $char;
+
+ $start = $i;
+
+ while (isset($chars[$i + 1]) && $chars[$i + 1] === $char) {
+ $string_part .= $char;
+ $i++;
+ }
+
+ // We need more info to make decisions about this run of delimiter chars.
+ $prev_char = html_entity_decode($chars[$start - 1] ?? ' ');
+ $next_char = html_entity_decode($chars[$i + 1] ?? ' ');
+
+ $prev_is_space = preg_match('/\s/u', $prev_char);
+ $prev_is_punct = $prev_is_space ? false : preg_match('/\pP/u', $prev_char);
+
+ $next_is_space = preg_match('/\s/u', $next_char);
+ $next_is_punct = $next_is_space ? false : preg_match('/\pP/u', $next_char);
+
+ $left_flanking = !$next_is_space && (!$next_is_punct || $prev_is_space || $prev_is_punct);
+ $right_flanking = !$prev_is_space && (!$prev_is_punct || $next_is_space || $next_is_punct);
+
+ $can_open = $left_flanking && ($char === '*' || !$right_flanking || $prev_is_punct);
+ $can_close = $right_flanking && ($char === '*' || !$left_flanking || $next_is_punct);
+
+ $length = strlen($string_part);
+
+ // Max length of strikethrough delimiter is two chars.
+ if ($char === '~' && $length > 2) {
+ $new_content[array_key_last($new_content)] .= $string_part;
+ $string_part = '';
+ break;
+ }
+
+ // Add a node for this delimiter run.
+ $new_content[] = [
+ 'type' => $char,
+ 'properties' => [
+ 'length' => $length,
+ 'active' => true,
+ 'can_open' => $can_open,
+ 'can_close' => $can_close,
+ 'position' => $i,
+ ],
+ 'content' => $string_part,
+ ];
+
+ $string_part = '';
+
+ break;
+
+ case '!':
+ if ($escaped || ($chars[$i + 1] ?? '') !== '[') {
+ $string_part .= $char;
+ $escaped = false;
+ break;
+ }
+
+ $i++;
+
+ if ($string_part !== '') {
+ $new_content[] = $string_part;
+ $string_part = '';
+ }
+
+ $new_content[] = [
+ 'type' => '![',
+ 'properties' => [
+ 'length' => 2,
+ 'active' => true,
+ 'can_open' => true,
+ 'can_close' => false,
+ 'position' => $i,
+ ],
+ 'content' => '![',
+ ];
+
+ break;
+
+ case '[':
+ if ($escaped) {
+ $string_part .= '[';
+ $escaped = false;
+ break;
+ }
+
+ if ($string_part !== '') {
+ $new_content[] = $string_part;
+ $string_part = '';
+ }
+
+ $new_content[] = [
+ 'type' => '[',
+ 'properties' => [
+ 'length' => 1,
+ 'active' => true,
+ 'can_open' => true,
+ 'can_close' => false,
+ 'position' => $i,
+ ],
+ 'content' => '[',
+ ];
+
+ break;
+
+ case ']':
+ if ($escaped) {
+ $string_part .= ']';
+ $escaped = false;
+ break;
+ }
+
+ if ($string_part !== '') {
+ $new_content[] = $string_part;
+ $string_part = '';
+ }
+
+ $this->parseLink($chars, $i, $new_content);
+
+ break;
+
+ default:
+ if ($escaped) {
+ if (!in_array($char, self::ESCAPEABLE)) {
+ $string_part .= '\\';
+ }
+
+ $escaped = false;
+ }
+
+ $string_part .= $char;
+ break;
+ }
+ }
+
+ if ($string_part !== '') {
+ $new_content[] = $string_part;
+ }
+ }
+
+ // Change any remaining link/image delimiters into plain text.
+ foreach ($new_content as $key => $value) {
+ if (is_array($value) && in_array($value['type'], ['[', '!['])) {
+ $new_content[$key] = $value['content'];
+ }
+ }
+
+ // Amalgamate contiguous plain strings.
+ $this->amalgamateStrings($new_content);
+
+ // Clean up the keys.
+ $new_content = array_values($new_content);
+
+ // Process the emphasis and strikethrough delimiters.
+ $this->parseEmphasis($new_content);
+
+ // Clean up the keys again.
+ $new_content = array_values($new_content);
+
+ // Trim whitespace from the beginning of the paragraph.
+ if (is_string($new_content[0])) {
+ $new_content[0] = Utils::htmlTrimLeft($new_content[0]);
+ }
+
+ // Trim whitespace from the end of the paragraph.
+ if (is_string($new_content[array_key_last($new_content)])) {
+ $new_content[array_key_last($new_content)] = Utils::htmlTrimRight($new_content[array_key_last($new_content)]);
+ }
+
+ return $new_content;
+ }
+
+ /**
+ * Parses Markdown links.
+ *
+ * @param array $chars The characters of the line in which the link occurs.
+ * @param int &$i The index of the current character within $chars.
+ * @param array &$content The 'content' array of the block in which the link
+ * occurs.
+ */
+ protected function parseLink(array $chars, int &$i, array &$content): void
+ {
+ for ($c = array_key_last($content); $c >= 0; $c--) {
+ if (
+ !isset($content[$c])
+ || !is_array($content[$c])
+ || !in_array($content[$c]['type'], ['[', '!['])
+ ) {
+ continue;
+ }
+
+ $delim = &$content[$c];
+
+ $str = implode('', array_slice($chars, $delim['properties']['position'], $i - $delim['properties']['position'])) . ']' . mb_substr(implode('', $chars), $i + 1);
+
+ $prefix = $delim['type'] === '![' ? '!' : '';
+
+ // Inline link/image?
+ if (preg_match('~^' . $prefix . self::REGEX_LINK_INLINE . '~u', $str, $matches)) {
+ $this->parseEmphasis($content, $c);
+
+ $text = array_slice($content, $c + 1);
+ $this->amalgamateStrings($text);
+
+ $url = $this->extractLinkUrl($matches['destination'] ?? '');
+ $title = $this->extractLinkTitle($matches['title'] ?? '');
+
+ $content = array_slice($content, 0, $c);
+ $content[] = [
+ 'type' => $delim['type'] === '![' ? 'image' : 'link',
+ 'properties' => [
+ 'url' => $url,
+ 'title' => $title,
+ ],
+ 'content' => $text,
+ ];
+
+ $i += mb_strlen($matches[0]) - mb_strlen($matches['text']);
+
+ return;
+ }
+
+ // Reference link/image? Check for all three possible forms.
+ foreach ([
+ self::REGEX_LINK_REF_FULL,
+ self::REGEX_LINK_REF_COLLAPSED,
+ self::REGEX_LINK_REF_SHORTCUT,
+ ] as $regex) {
+ if (preg_match('~' . $prefix . $regex . '~u', $str, $matches)) {
+ break;
+ }
+ }
+
+ // If none of the forms matched, move on.
+ if (empty($matches)) {
+ $delim = $delim['content'];
+ $content[] = ']';
+
+ return;
+ }
+
+ $label_content = $this->extractLinkLabel($matches['label']);
+
+ if (isset($this->link_reference_definitions[$label_content])) {
+ $this->parseEmphasis($content, $c);
+
+ $text = array_slice($content, $c + 1);
+ $this->amalgamateStrings($text);
+
+ $url = $this->link_reference_definitions[$label_content]['url'];
+ $title = $this->link_reference_definitions[$label_content]['title'];
+
+ $content = array_slice($content, 0, $c);
+ $content[] = [
+ 'type' => $delim['type'] === '![' ? 'image' : 'link',
+ 'properties' => [
+ 'url' => $url,
+ 'title' => $title,
+ ],
+ 'content' => $text,
+ ];
+
+ $i += mb_strlen($matches[0]) - mb_strlen($matches['text'] ?? $matches['label']);
+ } else {
+ $delim = $delim['content'];
+ $content[] = ']';
+ }
+
+ return;
+ }
+
+ // If we get here, we didn't find a valid opening delimiter.
+ $content[] = ']';
+ }
+
+ /**
+ * Parses Markdown emphasis and strikethrough text.
+ *
+ * @param array &$content The 'content' array of the block in which the
+ * emphasis occurs.
+ * @param int $start_after Parsing will only happen in elements of $content
+ * whose keys are greater than this. Used to avoid parsing in parts of a
+ * string that have already been parsed. Default: -1.
+ */
+ protected function parseEmphasis(array &$content, int $start_after = -1): void
+ {
+ foreach ([false, true] as $allow_partial) {
+ $content = array_values($content);
+
+ foreach ($content as $key => $node) {
+ if (is_array($node) && in_array($node['type'], ['*', '_', '~'])) {
+ $content[$key]['properties']['active'] = true;
+ }
+ }
+
+ // Walk forward in the list until we reach the first item past $start_after.
+ reset($content);
+
+ while (key($content) !== null && key($content) <= $start_after) {
+ next($content);
+ }
+
+ while (key($content) !== null) {
+ unset($closer, $closer_key, $opener, $opener_key);
+
+ // Walk forward until we find a potential closing delimiter run.
+ while (key($content) !== null) {
+ $node = current($content);
+
+ if (
+ is_array($node)
+ && in_array($node['type'], ['*', '_', '~'])
+ && $node['properties']['can_close']
+ && $node['properties']['active']
+ ) {
+ break;
+ }
+
+ next($content);
+ }
+
+ if (
+ is_array($node)
+ && in_array($node['type'], ['*', '_', '~'])
+ && $node['properties']['can_close']
+ && $node['properties']['active']
+ ) {
+ $closer_key = key($content);
+ $closer = &$content[$closer_key];
+ } else {
+ continue;
+ }
+
+ // Walk back until we find a matching opening delimiter run.
+ do {
+ $node = prev($content);
+
+ if (key($content) === null || key($content) === $start_after) {
+ $closer['properties']['active'] = false;
+
+ reset($content);
+
+ while (
+ key($content) <= $closer_key
+ && key($content) < array_key_last($content)
+ ) {
+ next($content);
+ }
+
+ continue 2;
+ }
+ } while (
+ key($content) !== null
+ && key($content) > $start_after
+ && !(
+ is_array($node)
+ && $node['type'] === $closer['type']
+ && $node['properties']['can_open']
+ && (
+ $allow_partial
+ || $this->checkRuleOfThree($node, $closer)
+ )
+ )
+ );
+
+ if (is_array($node) && key($content) > $start_after) {
+ $opener_key = key($content);
+ $opener = &$content[$opener_key];
+ } else {
+ continue;
+ }
+
+ // Build the new version of the content.
+ $max_length = min($opener['properties']['length'], $closer['properties']['length'], 2);
+ $enclosed = array_slice($content, $opener_key + 1, $closer_key - $opener_key - 1);
+
+ $temp = array_slice($content, 0, $opener_key);
+
+ if ($opener['properties']['length'] > $max_length) {
+ $new_opener = $opener;
+
+ $new_opener['properties']['length'] -= $max_length;
+ $new_opener['content'] = substr($new_opener['content'], $max_length);
+
+ $temp[] = $new_opener;
+ }
+
+ foreach ($enclosed as $k => $n) {
+ if (is_array($n) && in_array($n['type'], ['*', '_', '~'])) {
+ $enclosed[$k] = $n['content'];
+ }
+ }
+
+ $temp[] = [
+ 'type' => $opener['type'] === '~' ? 's' : ($max_length === 1 ? 'i' : 'b'),
+ 'properties' => [],
+ 'content' => $enclosed,
+ ];
+
+ if ($closer['properties']['length'] > $max_length) {
+ $new_closer = $closer;
+
+ $new_closer['properties']['length'] -= $max_length;
+ $new_closer['content'] = substr($new_closer['content'], $max_length);
+
+ $temp[] = $new_closer;
+ }
+
+ $content = array_values(array_merge($temp, array_slice($content, $closer_key + 1)));
+
+ // Move the internal pointer to position just after the closer.
+ reset($content);
+
+ while (key($content) < $closer_key) {
+ next($content);
+
+ if (key($content) === null) {
+ break;
+ }
+ }
+ }
+ }
+
+ foreach ($content as $key => $node) {
+ if (is_array($node) && in_array($node['type'], ['*', '_', '~'])) {
+ $content[$key] = $node['content'];
+ }
+ }
+
+ $content = array_values(array_filter($content, fn ($arg) => $arg !== ''));
+ }
+
+ /**
+ * Helper method for $this->parseEmphasis().
+ *
+ * https://github.github.com/gfm/#emphasis-and-strong-emphasis:
+ *
+ * "If one of the delimiters can both open and close emphasis, then the sum
+ * of the lengths of the delimiter runs containing the opening and closing
+ * delimiters must not be a multiple of 3 unless both lengths are multiples
+ * of 3."
+ *
+ * This rule is used to ensure that, e.g., `*foo**bar**baz*` becomes
+ * `foobar baz ` rather than `foo bar baz `.
+ *
+ * @param array $opener Info about opening run of delimiter characters.
+ * @param array $closer Info about closing run of delimiter characters.
+ * @return bool Whether this combination of opener and closer is allowed.
+ */
+ protected function checkRuleOfThree(array $opener, array $closer): bool
+ {
+ // This rule doesn't apply to strikethrough text.
+ if ($opener['type'] === '~') {
+ return true;
+ }
+
+ if (($opener['properties']['length'] + $closer['properties']['length']) % 3 !== 0) {
+ return true;
+ }
+
+ return $opener['properties']['length'] % 3 === 0 && $closer['properties']['length'] % 3 === 0;
+ }
+
+ /**
+ * Extracts and normalizes the text for a link label.
+ *
+ * Note that link labels and link text might look identical a first glance,
+ * but are not quite the same thing.
+ *
+ * A link label is the part between square brackets in a link reference.
+ * Because link labels are used to match link references to link reference
+ * definitions, they are normalized for both case and white space so that
+ * the matching can happen while ignoring case and white space.
+ *
+ * Link text is the part between square brackets in an inline link. Because
+ * link text never needs to match with anything else, it does not need to be
+ * normalized for case or white space.
+ *
+ * @param string $label The link's label component, including syntax chars.
+ * @return string The normalized text of the link's label.
+ */
+ protected function extractLinkLabel(string $label = ''): string
+ {
+ return Utils::convertCase(preg_replace('/\s+/u', ' ', Utils::htmlTrim(substr($label, 1, -1))), 'fold');
+ }
+
+ /**
+ * Extracts the URL for a link.
+ *
+ * @param string $destination The link's URL component, including syntax chars.
+ * @return string The text of the link's URL.
+ */
+ protected function extractLinkUrl(string $destination = ''): string
+ {
+ return Utils::htmlTrim(trim($destination, '<>'));
+ }
+
+ /**
+ * Extracts the title for a link.
+ *
+ * @param string $title The link's title component, including syntax chars.
+ * @return string The text of the link's title.
+ */
+ protected function extractLinkTitle(string $title = ''): string
+ {
+ if (strlen($title) < 2) {
+ return $title;
+ }
+
+ return substr(Utils::htmlTrim($title), 1, -1);
+ }
+
+ /**
+ * Combines contiguous strings within a block's content.
+ *
+ * @param array &$content The 'content' array of the block in which the
+ * strings occur.
+ */
+ protected function amalgamateStrings(array &$content): void
+ {
+ for ($i = 0; $i < count($content); $i++) {
+ if (!isset($content[$i]) || !is_string($content[$i])) {
+ continue;
+ }
+
+ $c = $i;
+
+ while (isset($content[$i + 1]) && is_string($content[$i + 1])) {
+ $content[$c] .= $content[++$i];
+ unset($content[$i]);
+ }
+ }
+
+ $content = array_values($content);
+ }
+
+ /*
+ * Part 3: Rendering.
+ */
+
+ /**
+ * Transforms a block or string into its final output form and appends it
+ * to $this->rendered.
+ *
+ * If $element is a block from $this->structure (or is $this->structure
+ * itself), this method will recurse into $element's children.
+ *
+ * @param array|string $element A block or string to render.
+ */
+ protected function render(array|string $element): void
+ {
+ // Is it a string?
+ if (is_string($element)) {
+ if ($this->output_type !== self::OUTPUT_BBC) {
+ $element = htmlspecialchars($element, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, Utils::$context['character_set'], false);
+
+ if ($this->hard_breaks & self::BR_IN_PARAGRAPHS) {
+ $element = nl2br($element, false);
+ }
+ } elseif (!($this->hard_breaks & self::BR_IN_PARAGRAPHS)) {
+ $element = strtr($element, ["\n" => ' ']);
+ }
+
+ $this->rendered .= $element;
+
+ return;
+ }
+
+ // Do we have a rendering method for this element?
+ if (isset($this->render_methods[$element['type']])) {
+ $render_method = method_exists($this, $this->render_methods[$element['type']]) ? [$this, $this->render_methods[$element['type']]] : Utils::getCallable($this->render_methods[$element['type']]);
+
+ if (is_callable($render_method)) {
+ $render_method($element);
+
+ if ($this->output_type === self::OUTPUT_BBC) {
+ $this->rendered .= str_repeat("\n", $element['properties']['blank_after'] ?? 0);
+ } elseif ($this->hard_breaks & self::BR_LINES) {
+ $this->rendered .= str_repeat(" \n", max(0, ($element['properties']['blank_after'] ?? 1) - 1));
+ }
+
+ return;
+ }
+ }
+
+ // No rendering method for the element itself, so recurse into the content.
+ foreach ($element['content'] as $content_element) {
+ $this->render($content_element);
+ }
+ }
+
+ /**
+ * Renders code blocks.
+ *
+ * @param array $element A code block to render.
+ */
+ protected function renderCodeBlock(array $element): void
+ {
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ if ($element['content'] === []) {
+ return;
+ }
+
+ $this->rendered .= '[code]';
+ break;
+
+ case self::OUTPUT_HTML:
+ if ($element['content'] === []) {
+ return;
+ }
+
+ $bbc_type = !empty($element['properties']['info_string']) ? 'unparsed_equals_content' : 'unparsed_content';
+
+ foreach (BBCodeParser::getCodes() as $code) {
+ if ($code['tag'] === 'code' && $code['type'] === $bbc_type) {
+ list($before, $after) = preg_split('/\$1/', $code['content']);
+ break;
+ }
+ }
+
+ if (isset($this->disabled[$code['tag']])) {
+ $code = $this->disableCode($code);
+ }
+
+ $this->rendered .= strtr(
+ $before,
+ [
+ '{txt_code}' => Lang::getTxt('code'),
+ '{txt_code_select}' => Lang::getTxt('code_select'),
+ '{txt_code_shrink}' => Lang::getTxt('code_shrink'),
+ '{txt_code_expand}' => Lang::getTxt('quote_expand'),
+ '$2' => htmlspecialchars($element['properties']['info_string'] ?? ''),
+ ],
+ );
+ break;
+
+ default:
+ $info_string_words = Utils::extractWords($element['properties']['info_string'] ?? '');
+
+ $this->rendered .= '';
+ break;
+ }
+
+ $code_start = mb_strlen($this->rendered);
+
+ foreach ($element['content'] as $c => $content_element) {
+ $this->render($content_element);
+
+ if ($c !== array_key_last($element['content'])) {
+ $this->rendered .= "\n";
+ }
+ }
+
+ // Why not do some syntax highlighting?
+ if (
+ $this->output_type === self::OUTPUT_HTML
+ && strtoupper($element['properties']['info_string'] ?? '') === 'PHP'
+ ) {
+ $code = mb_substr($this->rendered, $code_start);
+
+ $add_begin = !str_starts_with(Utils::htmlTrim($code), '<?');
+
+ $code = BBCodeParser::highlightPhpCode($add_begin ? '<?php ' . $code . '?>' : $code);
+
+ if ($add_begin) {
+ $code = preg_replace(['/^(.+?)<\?.{0,40}?php(?: |\s)/u', '/\?>((?:\s*<\/(font|span)>)*)$/um'], '$1', $code, 2);
+ }
+
+ $this->rendered = mb_substr($this->rendered, 0, $code_start) . $code;
+ }
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[/code]';
+ break;
+
+ case self::OUTPUT_HTML:
+ $this->rendered .= $after;
+ break;
+
+ default:
+ $this->rendered .= "\n";
+ $this->rendered .= ' ';
+ break;
+ }
+
+ $this->rendered .= "\n";
+ }
+
+ /**
+ * Renders blockquotes.
+ *
+ * @param array $element A blockquote to render.
+ */
+ protected function renderBlockquote(array $element): void
+ {
+ static $nesting_level = 0;
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ if ($element['content'] === []) {
+ return;
+ }
+
+ $this->rendered .= '[quote]';
+ break;
+
+ case self::OUTPUT_HTML:
+ if ($element['content'] === []) {
+ return;
+ }
+
+ foreach (BBCodeParser::getCodes() as $code) {
+ if (
+ $code['tag'] === 'quote'
+ && !isset($code['type'])
+ && !isset($code['parameters'])
+ ) {
+ break;
+ }
+ }
+
+ if (isset($this->disabled[$code['tag']])) {
+ $code = $this->disableCode($code);
+ }
+
+ // Add a class to the quote to style nested blockquotes.
+ $code['before'] = strtr($code['before'], ['' => '']);
+
+ $this->rendered .= strtr($code['before'], ['{txt_quote}' => Lang::getTxt('quote')]);
+ break;
+
+ default:
+ $this->rendered .= '';
+ break;
+ }
+
+ $this->rendered .= "\n";
+
+ // Don't bother with single paragraphs inside blockquotes.
+ if (
+ count($element['content']) === 1
+ && is_array($element['content'][0])
+ && $element['content'][0]['type'] === 'p'
+ ) {
+ $element['content'] = $element['content'][0]['content'];
+ } elseif ($this->output_type !== self::OUTPUT_BBC) {
+ $this->rendered .= "\n";
+ }
+
+ foreach ($element['content'] as $c => $content_element) {
+ $nesting_level++;
+ $this->render($content_element);
+ $nesting_level--;
+
+ if (
+ $this->rendered === Utils::htmlTrimRight($this->rendered)
+ && $c !== array_key_last($element['content'])
+ && is_string($element['content'][$c])
+ && is_string($element['content'][$c + 1])
+ && $element['content'][$c + 1] === Utils::htmlTrimLeft($element['content'][$c + 1])
+ ) {
+ $this->rendered .= ' ';
+ }
+ }
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[/quote]';
+ break;
+
+ case self::OUTPUT_HTML:
+ $this->rendered .= $code['after'];
+ break;
+
+ default:
+ $this->rendered .= ' ';
+ break;
+ }
+
+ $this->rendered .= "\n";
+ }
+
+ /**
+ * Renders lists.
+ *
+ * @param array $element A list to render.
+ */
+ protected function renderList(array $element): void
+ {
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ if ($element['content'] === []) {
+ return;
+ }
+
+ $this->rendered .= '[list' . ($element['properties']['ordered'] ? ' type=decimal' : '') . ']';
+ break;
+
+ case self::OUTPUT_HTML:
+ if ($element['content'] === []) {
+ return;
+ }
+
+ $style_type = $element['properties']['ordered'] ? 'decimal' : 'disc';
+
+ foreach (BBCodeParser::getCodes() as $code) {
+ if (
+ $code['tag'] === 'list'
+ && isset($code['parameters']['type'])
+ && str_contains($code['parameters']['type']['match'], $style_type)
+ ) {
+ break;
+ }
+ }
+
+ if (isset($this->disabled[$code['tag']])) {
+ $code = $this->disableCode($code);
+ }
+
+ $this->rendered .= strtr($code['before'], ['{type}' => $style_type]);
+ break;
+
+ default:
+ $this->rendered .= $element['properties']['ordered'] ? '' : '';
+ break;
+ }
+
+ $this->rendered .= "\n";
+
+ foreach ($element['content'] as $content_element) {
+ $this->render($content_element);
+ }
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[/list]';
+ break;
+
+ case self::OUTPUT_HTML:
+ $this->rendered .= $code['after'];
+ break;
+
+ default:
+ $this->rendered .= $element['properties']['ordered'] ? ' ' : '';
+ break;
+ }
+
+ $this->rendered .= "\n";
+ }
+
+ /**
+ * Renders list items.
+ *
+ * @param array $element A list item to render.
+ */
+ protected function renderListItem(array $element): void
+ {
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ if ($element['content'] === []) {
+ return;
+ }
+
+ $this->rendered .= '[li]';
+ break;
+
+ case self::OUTPUT_HTML:
+ if ($element['content'] === []) {
+ return;
+ }
+
+ foreach (BBCodeParser::getCodes() as $code) {
+ if ($code['tag'] === 'li') {
+ break;
+ }
+ }
+
+ if (isset($this->disabled[$code['tag']])) {
+ $code = $this->disableCode($code);
+ }
+
+ $this->rendered .= $code['before'];
+ break;
+
+ default:
+ $this->rendered .= '';
+ break;
+ }
+
+ // Don't bother with single paragraphs inside list items.
+ if (
+ count($element['content']) === 1
+ && is_array($element['content'][0])
+ && $element['content'][0]['type'] === 'p'
+ ) {
+ $element['content'] = $element['content'][0]['content'];
+ } elseif ($this->output_type !== self::OUTPUT_BBC) {
+ $this->rendered .= "\n";
+ }
+
+ foreach ($element['content'] as $c => $content_element) {
+ $this->render($content_element);
+
+ if ($c === array_key_last($element['content'])) {
+ if ($this->output_type === self::OUTPUT_BBC) {
+ $this->rendered = Utils::htmlTrimRight($this->rendered);
+ }
+ } elseif (
+ $this->rendered === Utils::htmlTrimRight($this->rendered)
+ && is_string($element['content'][$c])
+ && is_string($element['content'][$c + 1])
+ && $element['content'][$c + 1] === Utils::htmlTrimLeft($element['content'][$c + 1])
+ ) {
+ $this->rendered .= ' ';
+ }
+ }
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[/li]';
+ break;
+
+ case self::OUTPUT_HTML:
+ $this->rendered .= $code['after'];
+ break;
+
+ default:
+ $this->rendered .= ' ';
+ break;
+ }
+
+ $this->rendered .= "\n";
+ }
+
+ /**
+ * Renders thematic breaks.
+ *
+ * @param array $element A thematic break to render.
+ */
+ protected function renderHr(array $element): void
+ {
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[hr]';
+ break;
+
+ case self::OUTPUT_HTML:
+ foreach (BBCodeParser::getCodes() as $code) {
+ if ($code['tag'] === 'hr') {
+ break;
+ }
+ }
+
+ if (isset($this->disabled[$code['tag']])) {
+ $code = $this->disableCode($code);
+ }
+
+ $this->rendered .= $code['content'];
+ break;
+
+ default:
+ $this->rendered .= ' ';
+ break;
+ }
+
+ $this->rendered .= "\n";
+ }
+
+ /**
+ * Renders headings.
+ *
+ * @param array $element A heading to render.
+ */
+ protected function renderHeading(array $element): void
+ {
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ if ($element['content'] === []) {
+ return;
+ }
+
+ $this->rendered .= '[h' . $element['properties']['level'] . ']';
+ break;
+
+ case self::OUTPUT_HTML:
+ if ($element['content'] === []) {
+ return;
+ }
+
+ foreach (BBCodeParser::getCodes() as $code) {
+ if ($code['tag'] === 'h' . $element['properties']['level']) {
+ break;
+ }
+ }
+
+ if (isset($this->disabled[$code['tag']])) {
+ $code = $this->disableCode($code);
+ }
+
+ $this->rendered .= $code['before'];
+ break;
+
+ default:
+ $this->rendered .= '';
+ break;
+ }
+
+ // Don't bother with paragraphs inside headings.
+ if (
+ count($element['content']) === 1
+ && is_array($element['content'][0])
+ && $element['content'][0]['type'] === 'p'
+ ) {
+ $element['content'] = $element['content'][0]['content'];
+ }
+
+ foreach ($element['content'] as $c => $content_element) {
+ $this->render($content_element);
+
+ if (
+ $this->rendered === Utils::htmlTrimRight($this->rendered)
+ && $c !== array_key_last($element['content'])
+ && is_string($element['content'][$c])
+ && is_string($element['content'][$c + 1])
+ && $element['content'][$c + 1] === Utils::htmlTrimLeft($element['content'][$c + 1])
+ ) {
+ $this->rendered .= ' ';
+ }
+ }
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[/h' . $element['properties']['level'] . ']';
+ break;
+
+ case self::OUTPUT_HTML:
+ foreach (BBCodeParser::getCodes() as $code) {
+ if ($code['tag'] === 'h' . $element['properties']['level']) {
+ break;
+ }
+ }
+
+ $this->rendered .= $code['after'];
+ $this->rendered .= "\n";
+ break;
+
+ default:
+ $this->rendered .= ' ';
+ break;
+ }
+
+ $this->rendered .= "\n";
+ }
+
+ /**
+ * Renders HTML blocks.
+ *
+ * @param array $element An HTML block to render.
+ */
+ protected function renderHtmlBlock(array $element): void
+ {
+ $this->rendered .= implode("\n", $element['content']) . "\n";
+ }
+
+ /**
+ * Renders inline HTML.
+ *
+ * @param array $element Some inline HTML to render.
+ */
+ protected function renderHtmlInline(array $element): void
+ {
+ $this->rendered .= implode("\n", $element['content']);
+ }
+
+ /**
+ * Renders tables.
+ *
+ * @param array $element A table to render.
+ */
+ protected function renderTable(array $element): void
+ {
+ $is_disabled = false;
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ if ($element['content'] === []) {
+ return;
+ }
+
+ $this->rendered .= '[table]';
+ break;
+
+ case self::OUTPUT_HTML:
+ if ($element['content'] === []) {
+ return;
+ }
+
+ foreach (BBCodeParser::getCodes() as $code) {
+ if ($code['tag'] === 'table') {
+ break;
+ }
+ }
+
+ if (isset($this->disabled[$code['tag']])) {
+ $code = $this->disableCode($code);
+ $is_disabled = true;
+ }
+
+ $this->rendered .= $code['before'];
+ break;
+
+ default:
+ $this->rendered .= '';
+ break;
+ }
+
+ $this->rendered .= "\n";
+
+ foreach ($element['content'] as $key => $row) {
+ if ($this->output_type !== self::OUTPUT_BBC && $key < 2 && !$is_disabled) {
+ $this->rendered .= $key === 0 ? '' : ' ';
+ $this->rendered .= "\n";
+ }
+
+ if (!$is_disabled) {
+ $this->rendered .= $this->output_type === self::OUTPUT_BBC ? '[tr]' : '';
+ $this->rendered .= "\n";
+ }
+
+ foreach ($row as $col => $cell) {
+ $cell['properties']['th'] = $this->output_type !== self::OUTPUT_BBC && $key === 0;
+ $cell['properties']['align'] = $element['properties']['align'][$col] ?? 'none';
+
+ $this->render($cell);
+ }
+
+ if (!$is_disabled) {
+ $this->rendered .= $this->output_type === self::OUTPUT_BBC ? '[/tr]' : ' ';
+ $this->rendered .= "\n";
+ }
+
+ if ($this->output_type !== self::OUTPUT_BBC && !$is_disabled) {
+ $this->rendered .= $key === 0 ? '' . "\n" : '';
+ }
+ }
+
+ if ($this->output_type !== self::OUTPUT_BBC && !$is_disabled) {
+ $this->rendered .= ' ';
+ $this->rendered .= "\n";
+ }
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[table]';
+ $this->rendered .= "\n";
+ break;
+
+ case self::OUTPUT_HTML:
+ $this->rendered .= $code['after'];
+ $this->rendered .= "\n";
+ break;
+
+ default:
+ $this->rendered .= '';
+ $this->rendered .= "\n";
+ break;
+ }
+ }
+
+ /**
+ * Renders table cells.
+ *
+ * @param array $element A table cell to render.
+ */
+ protected function renderTableCell(array $element): void
+ {
+ $is_disabled = $this->output_type === self::OUTPUT_HTML && isset($this->disabled['td']);
+
+ $tag = !empty($element['properties']['th']) ? 'th' : 'td';
+ $align = $element['properties']['align'] === 'none' ? null : $element['properties']['align'];
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[td]';
+
+ if (in_array($element['properties']['align'], ['left', 'right', 'center'])) {
+ $this->rendered .= '[' . $element['properties']['align'] . 'text]';
+ }
+ break;
+
+ case self::OUTPUT_HTML:
+ if ($is_disabled) {
+ break;
+ }
+ // no break
+
+ default:
+ $this->rendered .= '<' . $tag;
+
+ if (in_array($element['properties']['align'], ['left', 'right', 'center'])) {
+ $this->rendered .= ' style="text-align: ' . $element['properties']['align'] . '"';
+ }
+
+ $this->rendered .= '>';
+ break;
+ }
+
+ foreach ($element['content'] as $c => $content_element) {
+ $this->render($content_element);
+
+ if (
+ $this->rendered === Utils::htmlTrimRight($this->rendered)
+ && $c !== array_key_last($element['content'])
+ && is_string($element['content'][$c])
+ && is_string($element['content'][$c + 1])
+ && $element['content'][$c + 1] === Utils::htmlTrimLeft($element['content'][$c + 1])
+ ) {
+ $this->rendered .= ' ';
+ }
+ }
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ if (in_array($element['properties']['align'], ['left', 'right', 'center'])) {
+ $this->rendered .= '[/' . $element['properties']['align'] . 'text]';
+ }
+
+ $this->rendered .= '[/td]';
+ break;
+
+ case self::OUTPUT_HTML:
+ if ($is_disabled) {
+ break;
+ }
+ // no break
+
+ default:
+ $this->rendered .= '' . $tag . '>';
+ break;
+ }
+
+ $this->rendered .= "\n";
+ }
+
+ /**
+ * Renders paragraphs.
+ *
+ * @param array $element A paragraph to render.
+ */
+ protected function renderParagraph(array $element): void
+ {
+ if ($element['content'] === []) {
+ return;
+ }
+
+ $this->rendered .= $this->output_type === self::OUTPUT_BBC ? '' : '';
+
+ foreach ($element['content'] as $c => $content_element) {
+ $this->render($content_element);
+
+ if (
+ $this->rendered === Utils::htmlTrimRight($this->rendered)
+ && $c !== array_key_last($element['content'])
+ && is_string($element['content'][$c])
+ && is_string($element['content'][$c + 1])
+ && $element['content'][$c + 1] === Utils::htmlTrimLeft($element['content'][$c + 1])
+ ) {
+ $this->rendered .= ' ';
+ }
+ }
+
+ $this->rendered = Utils::htmlTrimRight($this->rendered);
+ $this->rendered .= $this->output_type === self::OUTPUT_BBC ? '' : '
';
+ $this->rendered .= "\n";
+ }
+
+ /**
+ * Renders links.
+ *
+ * @param array $element A link to render.
+ */
+ protected function renderLink(array $element): void
+ {
+ $bbc = str_starts_with($element['properties']['url'], Config::$boardurl) ? 'iurl' : 'url';
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ if (empty($element['properties']['url']) || empty($element['content'])) {
+ return;
+ }
+
+ $this->rendered .= '[' . $bbc . '="' . $element['properties']['url'] . '"]';
+ break;
+
+ case self::OUTPUT_HTML:
+ if (empty($element['properties']['url']) || empty($element['content'])) {
+ return;
+ }
+
+ foreach (BBCodeParser::getCodes() as $code) {
+ if ($code['tag'] === $bbc && $code['type'] === 'unparsed_equals') {
+ break;
+ }
+ }
+
+ if (isset($this->disabled[$code['tag']])) {
+ $code = $this->disableCode($code);
+ }
+
+ $this->rendered .= strtr($code['before'], ['$1' => $element['properties']['url']]);
+ break;
+
+ default:
+ $this->rendered .= ' 0 ? ' title="' . $element['properties']['title'] . '"' : '') . '>';
+ break;
+ }
+
+ foreach ($element['content'] as $content_element) {
+ $this->render($content_element);
+ }
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[/' . $bbc . ']';
+ break;
+
+ case self::OUTPUT_HTML:
+ $this->rendered .= strtr($code['after'], ['$1' => $element['properties']['url']]);
+ break;
+
+ default:
+ $this->rendered .= ' ';
+ break;
+ }
+ }
+
+ /**
+ * Renders images.
+ *
+ * @param array $element An image to render.
+ */
+ protected function renderImage(array $element): void
+ {
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ if (empty($element['properties']['url'])) {
+ return;
+ }
+
+ $this->rendered .= '[img]' . $element['properties']['url'] . '[/img]';
+ break;
+
+ case self::OUTPUT_HTML:
+ if (empty($element['properties']['url'])) {
+ return;
+ }
+
+ $this->rendered .= BBCodeParser::load()->parse('[img]' . $element['properties']['url'] . '[/img]');
+ break;
+
+ default:
+ $this->rendered .= ' 0 ? ' title="' . $element['properties']['title'] . '"' : '') . '>';
+ break;
+ }
+ }
+
+ /**
+ * Renders inline code.
+ *
+ * @param array $element A span of inline code to render.
+ */
+ protected function renderInlineCode(array $element): void
+ {
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[tt]';
+ break;
+
+ case self::OUTPUT_HTML:
+ foreach (BBCodeParser::getCodes() as $code) {
+ if ($code['tag'] === 'tt') {
+ break;
+ }
+ }
+
+ if (isset($this->disabled[$code['tag']])) {
+ $code = $this->disableCode($code);
+ }
+
+ $this->rendered .= $code['before'];
+ break;
+
+ default:
+ $this->rendered .= '';
+ break;
+ }
+
+ foreach ($element['content'] as $content_element) {
+ $this->render($content_element);
+ }
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[/tt]';
+ break;
+
+ case self::OUTPUT_HTML:
+ $this->rendered .= $code['after'];
+ break;
+
+ default:
+ $this->rendered .= '';
+ break;
+ }
+ }
+
+ /**
+ * Renders emphasis.
+ *
+ * @param array $element A span of emphasized text to render.
+ */
+ protected function renderEm(array $element): void
+ {
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[i]';
+ break;
+
+ case self::OUTPUT_HTML:
+ foreach (BBCodeParser::getCodes() as $code) {
+ if ($code['tag'] === 'i') {
+ break;
+ }
+ }
+
+ if (isset($this->disabled[$code['tag']])) {
+ $code = $this->disableCode($code);
+ }
+
+ $this->rendered .= $code['before'];
+ break;
+
+ default:
+ $this->rendered .= '';
+ break;
+ }
+
+ foreach ($element['content'] as $content_element) {
+ $this->render($content_element);
+ }
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[/i]';
+ break;
+
+ case self::OUTPUT_HTML:
+ $this->rendered .= $code['after'];
+ break;
+
+ default:
+ $this->rendered .= ' ';
+ break;
+ }
+ }
+
+ /**
+ * Renders strong emphasis.
+ *
+ * @param array $element A span of strongly emphasized text to render.
+ */
+ protected function renderStrong(array $element): void
+ {
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[b]';
+ break;
+
+ case self::OUTPUT_HTML:
+ foreach (BBCodeParser::getCodes() as $code) {
+ if ($code['tag'] === 'b') {
+ break;
+ }
+ }
+
+ if (isset($this->disabled[$code['tag']])) {
+ $code = $this->disableCode($code);
+ }
+
+ $this->rendered .= $code['before'];
+ break;
+
+ default:
+ $this->rendered .= '';
+ break;
+ }
+
+ foreach ($element['content'] as $content_element) {
+ $this->render($content_element);
+ }
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[/b]';
+ break;
+
+ case self::OUTPUT_HTML:
+ $this->rendered .= $code['after'];
+ break;
+
+ default:
+ $this->rendered .= ' ';
+ break;
+ }
+ }
+
+ /**
+ * Renders strikethrough text.
+ *
+ * @param array $element A span of strikethrough text to render.
+ */
+ protected function renderStrikethrough(array $element): void
+ {
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[s]';
+ break;
+
+ case self::OUTPUT_HTML:
+ foreach (BBCodeParser::getCodes() as $code) {
+ if ($code['tag'] === 's') {
+ break;
+ }
+ }
+
+ if (isset($this->disabled[$code['tag']])) {
+ $code = $this->disableCode($code);
+ }
+
+ $this->rendered .= $code['before'];
+ break;
+
+ default:
+ $this->rendered .= '';
+ break;
+ }
+
+ foreach ($element['content'] as $content_element) {
+ $this->render($content_element);
+ }
+
+ switch ($this->output_type) {
+ case self::OUTPUT_BBC:
+ $this->rendered .= '[/s]';
+ break;
+
+ case self::OUTPUT_HTML:
+ $this->rendered .= $code['after'];
+ break;
+
+ default:
+ $this->rendered .= '';
+ break;
+ }
+ }
+
+ /*
+ * Miscellaneous.
+ */
+
+ /**
+ * Helper method that returns the requested callable.
+ *
+ * This is used to allow $this->block_types to define the necessary methods
+ * for working with various block types.
+ *
+ * @param string|bool|null $method Either (a) a boolean to return, (b) null
+ * to do nothing, or (c) the name of a method, possibly prepended by '!'
+ * if the boolean inverse of the method's results are desired.
+ * @return A callable, a boolean, or null.
+ */
+ protected function getMethod(string|bool|null $method): mixed
+ {
+ if (is_null($method)) {
+ return null;
+ }
+
+ if (is_bool($method)) {
+ return function (...$args) use ($method) {
+ return $method;
+ };
+ }
+
+ $not = str_starts_with($method, '!');
+
+ $method = ltrim($method, '!');
+
+ if (!method_exists($this, $method)) {
+ return false;
+ }
+
+ if ($not) {
+ return function (...$args) use ($method) {
+ return !($this->$method(...$args));
+ };
+ } else {
+ return [$this, $method];
+ }
+ }
+
+ /**
+ * Resets runtime properties to their default values.
+ */
+ protected function resetRuntimeProperties(): void
+ {
+ // Reset these properties.
+ $to_reset = [
+ 'line_info',
+ 'structure',
+ 'open',
+ 'last_block',
+ 'link_reference_definitions',
+ 'in_code',
+ 'opening_fence_linenum',
+ 'opening_fence',
+ 'info_string',
+ 'in_html',
+ 'table_align',
+ 'placeholders',
+ 'rendered',
+ ];
+
+ $class_vars = get_class_vars(__CLASS__);
+
+ foreach ($to_reset as $var) {
+ unset($this->{$var});
+ $this->{$var} = $class_vars[$var];
+ }
+
+ // Ensure p is always the last element in $this->block_types.
+ if (array_key_last($this->block_types) !== 'p') {
+ $p = $this->block_types['p'];
+ unset($this->block_types['p']);
+ $this->block_types['p'] = $p;
+ }
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/Sources/Parsers/SmileyParser.php b/Sources/Parsers/SmileyParser.php
new file mode 100644
index 0000000000..7c8d34f96d
--- /dev/null
+++ b/Sources/Parsers/SmileyParser.php
@@ -0,0 +1,252 @@
+smiley_preg_replacements = [];
+ $search_parts = [];
+ $smileys_path = Utils::htmlspecialchars(self::$smileys_url . '/' . rawurlencode(self::$smiley_set) . '/');
+
+ foreach ($data as $id => $smiley) {
+ $special_chars = Utils::htmlspecialchars($smiley['code'], ENT_QUOTES);
+
+ $smiley_code = ' ';
+
+ $this->smiley_preg_replacements[$smiley['code']] = $smiley_code;
+
+ $search_parts[] = $smiley['code'];
+
+ if ($smiley['code'] != $special_chars) {
+ $this->smiley_preg_replacements[$special_chars] = $smiley_code;
+ $search_parts[] = $special_chars;
+
+ // Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
+ $special_chars2 = preg_replace('/(\d{2});/', '$1;', $special_chars);
+
+ if ($special_chars2 != $special_chars) {
+ $this->smiley_preg_replacements[$special_chars2] = $smiley_code;
+ $search_parts[] = $special_chars2;
+ }
+ }
+ }
+
+ // This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:David@bla.com] doesn't parse the :D smiley)
+ $this->smiley_preg_search = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?smiley_preg_search, &$this->smiley_preg_replacements]);
+ }
+ }
+
+ /**
+ * Parse smileys in the passed string.
+ *
+ * The smiley parsing function which makes pretty faces appear :).
+ * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
+ * These are specifically not parsed in code tags [url=mailto:Dad@blah.com]
+ * Caches the smileys from the database or array in memory.
+ *
+ * @param string $string The string to parse smileys in.
+ * @return string The string with smiley images inserted.
+ */
+ public function parse(string $string): string
+ {
+ if (
+ self::$smiley_set == 'none'
+ || !isset($this->smiley_preg_search)
+ || empty($this->smiley_preg_replacements)
+ || trim($string) == ''
+ ) {
+ return $string;
+ }
+
+ // Don't parse smileys inside HTML or BBCode tags.
+ $parts = preg_split('~(<[^>]*>|\[\/?' . BBCodeParser::load()->getAllTagsRegex() . '[^\]]*\])~u', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+ for ($i = 0; $i < count($parts); $i++) {
+ if ($i % 2 === 0) {
+ $parts[$i] = preg_replace_callback(
+ $this->smiley_preg_search,
+ fn ($matches) => $this->smiley_preg_replacements[$matches[1]],
+ $parts[$i],
+ );
+ }
+ }
+
+ return implode('', $parts);
+ }
+
+ /**
+ * Converts HTML img tags for smileys back into smiley text.
+ *
+ * @param string $string Text containing HTML.
+ * @return string The string with smiley images converted to text.
+ */
+ public function unparse(string $string): string
+ {
+ $smiley_codes = array_map(fn ($smiley) => $smiley['code'], self::loadData(''));
+
+ return preg_replace_callback(
+ '~(\h?) ]+alt="([^"]+)"[^>]+class="smiley"[^>]*>(\h?)~i',
+ fn ($match) => in_array(html_entity_decode($match[2]), $smiley_codes) ? $match[1] . html_entity_decode($match[2]) . $match[3] : $match[0],
+ $string,
+ );
+ }
+
+ /************************
+ * Public static methods.
+ ************************/
+
+ /**
+ * Returns a reusable instance of this class.
+ *
+ * Using this method to get a SmileyParser instance saves memory by avoiding
+ * creating redundant instances.
+ *
+ * @return object An instance of this class.
+ */
+ public static function load(): object
+ {
+ if (!isset(self::$parser)) {
+ self::$parser = new self();
+ }
+
+ return self::$parser;
+ }
+
+ /**
+ * Loads data for the requested smiley set from the database.
+ *
+ * @param string $set The name of the smiley set.
+ * @return array Data for all the smileys in the specified set.
+ */
+ public static function loadData(string $set): array
+ {
+ if ($set === 'none') {
+ return [];
+ }
+
+ if (isset(self::$data[$set])) {
+ return self::$data[$set];
+ }
+
+ // Cache for longer when customized smiley codes aren't enabled
+ $cache_time = !self::$custom_smileys_enabled ? 7200 : 480;
+ $cache_key = 'parsing_smileys' . ($set !== '' ? '_' . $set : '');
+
+ if (is_array($data = CacheApi::get($cache_key, $cache_time))) {
+ self::$data[$set] = $data;
+
+ return self::$data[$set];
+ }
+
+ // Load the smileys in reverse order by length so they don't get parsed incorrectly.
+ self::$data[$set] = [];
+
+ $request = Db::$db->query(
+ '',
+ 'SELECT s.id_smiley, s.code, f.filename, s.description
+ FROM {db_prefix}smileys AS s
+ JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
+ WHERE ' . ($set !== '' ? 'f.smiley_set = {string:smiley_set}' : '1=1') . (!self::$custom_smileys_enabled ? '
+ AND s.code IN ({array_string:default_codes})' : '') . '
+ ORDER BY LENGTH(s.code) DESC',
+ [
+ 'default_codes' => ['>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'],
+ 'smiley_set' => $set,
+ ],
+ );
+
+ while ($row = Db::$db->fetch_assoc($request)) {
+ self::$data[$set][(int) $row['id_smiley']] = [
+ 'code' => $row['code'],
+ 'filename' => $row['filename'],
+ 'description' => !empty(Lang::$txt['icon_' . strtolower($row['description'])]) ? Lang::$txt['icon_' . strtolower($row['description'])] : $row['description'],
+ ];
+ }
+
+ Db::$db->free_result($request);
+
+ CacheApi::put($cache_key, self::$data[$set], $cache_time);
+
+ return self::$data[$set];
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/Sources/Parsers/index.php b/Sources/Parsers/index.php
new file mode 100644
index 0000000000..976d292448
--- /dev/null
+++ b/Sources/Parsers/index.php
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/Sources/PersonalMessage/DraftPM.php b/Sources/PersonalMessage/DraftPM.php
index 19b6337e66..94b8d3fa55 100644
--- a/Sources/PersonalMessage/DraftPM.php
+++ b/Sources/PersonalMessage/DraftPM.php
@@ -15,12 +15,12 @@
namespace SMF\PersonalMessage;
-use SMF\BBCodeParser;
use SMF\Config;
use SMF\Db\DatabaseApi as Db;
use SMF\Draft;
use SMF\Lang;
use SMF\PageIndex;
+use SMF\Parser;
use SMF\Theme;
use SMF\Time;
use SMF\User;
@@ -272,7 +272,10 @@ public static function showInProfile(int $memID = -1): void
Lang::censorText($row['subject']);
// BBC-ilize the message.
- $row['body'] = BBCodeParser::load()->parse($row['body'], true, 'draft' . $row['id_draft']);
+ $row['body'] = Parser::transform(
+ string: $row['body'],
+ options: ['cache_id' => 'draft' . $row['id_draft']],
+ );
// Have they provide who this will go to?
$recipients = [
diff --git a/Sources/PersonalMessage/PM.php b/Sources/PersonalMessage/PM.php
index a04c66b502..8b112bba12 100644
--- a/Sources/PersonalMessage/PM.php
+++ b/Sources/PersonalMessage/PM.php
@@ -19,7 +19,6 @@
use SMF\Actions\PersonalMessage as PMAction;
use SMF\ArrayAccessHelper;
use SMF\Autolinker;
-use SMF\BBCodeParser;
use SMF\Cache\CacheApi;
use SMF\Config;
use SMF\Db\DatabaseApi as Db;
@@ -31,6 +30,7 @@
use SMF\Mail;
use SMF\Menu;
use SMF\Msg;
+use SMF\Parser;
use SMF\Security;
use SMF\Theme;
use SMF\Time;
@@ -402,7 +402,10 @@ public function format(int $counter = 0, array $format_options = []): array
}
// Run BBC interpreter on the message.
- $this->formatted['body'] = BBCodeParser::load()->parse($this->formatted['body'], true, 'pm' . $this->id);
+ $this->formatted['body'] = Parser::transform(
+ string: $this->formatted['body'],
+ options: ['cache_id' => 'pm' . $this->id],
+ );
return $this->formatted;
}
@@ -533,7 +536,7 @@ public static function get(int|array $ids, array $query_customizations = []): \G
// There will never be an ID 0, but SMF doesn't like empty arrays when you tell it to expect an array of integers...
$params['ids'] = empty($ids) ? [0] : array_filter(array_unique(array_map('intval', $ids)));
- foreach(self::queryData($selects, $params, $joins, $where, $order, $group, $limit) as $row) {
+ foreach (self::queryData($selects, $params, $joins, $where, $order, $group, $limit) as $row) {
$id = (int) $row['id_pm'];
yield (new self($id, $row));
@@ -1001,7 +1004,12 @@ public static function compose2(): bool
Msg::preparsecode($message);
// Make sure there's still some content left without the tags.
- if (Utils::htmlTrim(strip_tags(BBCodeParser::load()->parse(Utils::htmlspecialchars($message, ENT_QUOTES), false), ' ')) === '' && (!User::$me->allowedTo('bbc_html') || !str_contains($message, '[html]'))) {
+ $temp = Parser::transform(
+ string: Utils::htmlspecialchars($message, ENT_QUOTES),
+ input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN,
+ );
+
+ if (Utils::htmlTrim(strip_tags($temp, ' ')) === '' && (!User::$me->allowedTo('bbc_html') || !str_contains($message, '[html]'))) {
$post_errors[] = 'no_message';
}
}
@@ -1029,7 +1037,7 @@ public static function compose2(): bool
Msg::preparsecode(Utils::$context['preview_message'], true);
// Parse out the BBC if it is enabled.
- Utils::$context['preview_message'] = BBCodeParser::load()->parse(Utils::$context['preview_message']);
+ Utils::$context['preview_message'] = Parser::transform(Utils::$context['preview_message']);
// Censor, as always.
Lang::censorText(Utils::$context['preview_subject']);
@@ -1569,7 +1577,22 @@ public static function send(array $recipients, string $subject, string $message,
Lang::censorText($notification_texts[$lang]['body']);
- $notification_texts[$lang]['body'] = trim(Utils::htmlspecialcharsDecode(strip_tags(strtr(BBCodeParser::load()->parse(Utils::htmlspecialchars($notification_texts[$lang]['body']), false), [' ' => "\n", '' => "\n", '' => "\n", '[' => '[', ']' => ']']))));
+ $notification_texts[$lang]['body'] = Parser::transform(
+ string: Utils::htmlspecialchars($notification_texts[$lang]['body']),
+ input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN,
+ output_type: Parser::OUTPUT_TEXT,
+ options: [
+ 'text_replacements' => [
+ ' ' => "\n",
+ '' => "\n",
+ '' => "\n",
+ '[' => '[',
+ ']' => ']',
+ ],
+ ],
+ );
+
+ $notification_texts[$lang]['body'] = trim(Utils::htmlspecialcharsDecode($notification_texts[$lang]['body']));
} else {
$notification_texts[$lang]['body'] = '';
}
@@ -2111,6 +2134,11 @@ public static function reportErrors(array $error_types, array $named_recipients,
Lang::censorText($row_quoted['subject']);
Lang::censorText($row_quoted['body']);
+ $row_quoted['body'] = Parser::transform(
+ string: $row_quoted['body'],
+ options: ['cache_id' => 'pm' . $row_quoted['id_pm']],
+ );
+
Utils::$context['quoted_message'] = [
'id' => $row_quoted['id_pm'],
'pm_head' => $row_quoted['pm_head'],
@@ -2124,7 +2152,7 @@ public static function reportErrors(array $error_types, array $named_recipients,
'subject' => $row_quoted['subject'],
'time' => Time::create('@' . $row_quoted['msgtime'])->format(),
'timestamp' => $row_quoted['msgtime'],
- 'body' => BBCodeParser::load()->parse($row_quoted['body'], true, 'pm' . $row_quoted['id_pm']),
+ 'body' => $row_quoted['body'],
];
}
diff --git a/Sources/Poll.php b/Sources/Poll.php
index a1eb446408..8dd588f377 100644
--- a/Sources/Poll.php
+++ b/Sources/Poll.php
@@ -365,7 +365,7 @@ public function format(array $format_options = []): array
$this->formatted = [
'id' => $this->id ?? 0,
'image' => 'normal_' . (empty($this->voting_locked) ? 'poll' : 'locked_poll'),
- 'question' => BBCodeParser::load()->parse($this->question),
+ 'question' => Parser::transform($this->question),
'max_votes' => $this->max_votes,
'total_votes' => $this->total_voters,
'guest_vote' => $this->guest_vote,
@@ -422,6 +422,8 @@ public function format(array $format_options = []): array
$bar = round(($option->votes * 100) / $divisor, $precision);
$barWide = $bar == 0 ? 1 : floor(($bar * 8) / 3);
+ $label = Parser::transform($option->label);
+
// Now add it to the poll's contextual theme data.
$this->formatted['choices'][$i] = [
'id' => 'options-' . $i,
@@ -431,7 +433,7 @@ public function format(array $format_options = []): array
'voted_this' => $option->voted_this != -1,
'bar_ndt' => $bar > 0 ? '
' : '',
'bar_width' => $barWide,
- 'label' => BBCodeParser::load()->parse($option->label),
+ 'label' => $label,
'vote_button' => ' ',
];
diff --git a/Sources/Profile.php b/Sources/Profile.php
index 1d1b85ed1a..54e7976f6e 100644
--- a/Sources/Profile.php
+++ b/Sources/Profile.php
@@ -1117,7 +1117,7 @@ public function loadCustomFields(string $area = 'summary'): void
// Parse BBCode
if ($cf_def['bbc']) {
- $output_html = BBCodeParser::load()->parse($output_html);
+ $output_html = Utils::adjustHeadingLevels(Parser::transform($output_html), null);
} elseif ($cf_def['field_type'] == 'textarea') {
// Allow for newlines at least
$output_html = strtr($output_html, ["\n" => ' ']);
@@ -1350,7 +1350,15 @@ public function loadSignatureData(): bool
Lang::censorText($signature);
- Utils::$context['member']['signature_preview'] = BBCodeParser::load()->parse($signature, true, 'sig' . $this->id, BBCodeParser::getSigTags());
+ Utils::$context['member']['signature_preview'] = Parser::transform(
+ string: $signature,
+ options: [
+ 'cache_id' => 'sig' . $this->id,
+ 'parse_tags' => Parser::getSigTags(),
+ ],
+ );
+
+ Utils::$context['member']['signature_preview'] = Utils::adjustHeadingLevels(Utils::$context['member']['signature_preview'], null);
Utils::$context['member']['signature'] = $_POST['signature'];
}
@@ -1926,8 +1934,8 @@ public static function validateSignature(string &$value): bool|string
return 'signature_max_image_count';
}
- // What about too many smileys!
- $smiley_parsed = BBCodeParser::load()->parseSmileys($unparsed_signature);
+ // What about too many smileys?
+ $smiley_parsed = Parser::transform($unparsed_signature, Parser::INPUT_SMILEYS);
$smiley_count = substr_count(strtolower($smiley_parsed), ' prepareString()
- */
- private BBCodeParser $bbcparser;
-
/****************
* Public methods
****************/
@@ -310,8 +303,7 @@ public function indexedWordQuery(array $words, array $search_data): mixed
}
} else {
foreach (
- array_values(array_intersect_key($this->wildcard_words, array_flip($words['all_words'])))
- as $key => $wildcard_word
+ array_values(array_intersect_key($this->wildcard_words, array_flip($words['all_words']))) as $key => $wildcard_word
) {
$wildcard_word = !empty($this->params['ignore_accents']) ? $this->removeAccents($wildcard_word) : $this->escapeAccents($wildcard_word);
@@ -1018,18 +1010,6 @@ protected function save(array $word_data): void
*/
protected function prepareString(string $string): string
{
- if (!isset($this->bbcparser)) {
- // BBCodeParser complains if User::$me is not set.
- if (!isset(User::$me)) {
- User::setMe(0);
- }
-
- $this->bbcparser = new BBCodeParser();
-
- // Leave out anything that would be skipped for printing.
- $this->bbcparser->for_print = true;
- }
-
// Disable image proxy because we want the original URLs.
$image_proxy_enabled = Config::$image_proxy_enabled ?? false;
Config::$image_proxy_enabled = false;
@@ -1044,8 +1024,18 @@ protected function prepareString(string $string): string
$images = $_GET['images'] ?? null;
unset($_GET['images']);
- // Parse the BBCode.
- $string = $this->bbcparser->parse($string, false);
+ // Parse the BBCode and Markdown.
+ $string = Parser::transform(
+ string: $string,
+ input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN,
+ output_type: Parser::OUTPUT_TEXT,
+ options: [
+ 'for_print' => true,
+ 'preg_replace' => [
+ '/<[^>]+>/' => fn ($matches) => $matches[0] . ' ',
+ ],
+ ],
+ );
// Put stuff back the way we found it.
Config::$image_proxy_enabled = $image_proxy_enabled;
@@ -1055,9 +1045,6 @@ protected function prepareString(string $string): string
$_GET['images'] = $images;
}
- // Remove HTML.
- $string = strip_tags(is_string($string) ? preg_replace('/<[^>]+>/', '$0 ', $string) : '');
-
// Decode 4-byte Unicode characters.
$string = mb_decode_numericentity($string, [0x010000, 0x10FFFF, 0, 0xFFFFFF], 'UTF-8');
diff --git a/Sources/Search/SearchApi.php b/Sources/Search/SearchApi.php
index 1eac4d33bb..df7048ebc1 100644
--- a/Sources/Search/SearchApi.php
+++ b/Sources/Search/SearchApi.php
@@ -17,13 +17,13 @@
use SMF\Actions\Search;
use SMF\BackwardCompatibility;
-use SMF\BBCodeParser;
use SMF\Config;
use SMF\Db\DatabaseApi as Db;
use SMF\ErrorHandler;
use SMF\IntegrationHook;
use SMF\Lang;
use SMF\PackageManager\SubsPackage;
+use SMF\Parser;
use SMF\User;
use SMF\Utils;
@@ -1016,7 +1016,7 @@ protected function setBlacklistedWords(): void
// Blacklist the BBC tags.
$this->blacklisted_words = array_unique(array_merge(
$this->blacklisted_words,
- array_map(fn ($code) => $code['tag'], BBCodeParser::getCodes()),
+ array_map(fn ($code) => $code['tag'], Parser::getBBCodes()),
));
IntegrationHook::call('integrate_search_blacklisted_words', [&$this->blacklisted_words]);
diff --git a/Sources/Search/SearchResult.php b/Sources/Search/SearchResult.php
index 3a5959bb91..749d9e2a53 100644
--- a/Sources/Search/SearchResult.php
+++ b/Sources/Search/SearchResult.php
@@ -16,11 +16,11 @@
namespace SMF\Search;
use SMF\Autolinker;
-use SMF\BBCodeParser;
use SMF\Config;
use SMF\Db\DatabaseApi as Db;
use SMF\IP;
use SMF\Lang;
+use SMF\Parser;
use SMF\Theme;
use SMF\Time;
use SMF\User;
@@ -210,7 +210,13 @@ public function format(int $counter = 0, array $format_options = []): array
$charLimit = 50;
$this->body = strtr($this->body, ["\n" => ' ', ' ' => "\n", ' ' => "\n", ' ' => "\n"]);
- $this->body = BBCodeParser::load()->parse($this->body, $this->smileys_enabled, $this->id_msg);
+
+ $this->body = Parser::transform(
+ string: $this->body,
+ input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ($this->smileys_enabled ? Parser::INPUT_SMILEYS : 0),
+ options: ['cache_id' => $this->id_msg],
+ );
+
$this->body = strip_tags(strtr($this->body, ['' => ' ', '' => ' ']), ' ');
if (Utils::entityStrlen($this->body) > $charLimit) {
@@ -254,7 +260,11 @@ public function format(int $counter = 0, array $format_options = []): array
$this->body_highlighted = self::highlight($this->body, SearchApi::$loadedApi->searchArray);
} else {
// Run BBC interpreter on the message.
- $this->body = BBCodeParser::load()->parse($this->body, $this->smileys_enabled, $this->id_msg);
+ $this->body = Parser::transform(
+ string: $this->body,
+ input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ($this->smileys_enabled ? Parser::INPUT_SMILEYS : 0),
+ options: ['cache_id' => $this->id_msg],
+ );
$this->subject_highlighted = self::highlight($this->subject, SearchApi::$loadedApi->searchArray);
$this->body_highlighted = self::highlight($this->body, SearchApi::$loadedApi->searchArray);
@@ -471,7 +481,7 @@ public static function get(int|array $ids, array $query_customizations = []): \G
$params['message_list'] = self::$messages_to_get = array_filter(array_unique(array_map('intval', (array) $ids)));
}
- foreach(self::queryData($selects, $params, $joins, $where, $order, $group, $limit) as $row) {
+ foreach (self::queryData($selects, $params, $joins, $where, $order, $group, $limit) as $row) {
$id = (int) $row['id_msg'];
yield (new self($id, $row));
diff --git a/Sources/ServerSideIncludes.php b/Sources/ServerSideIncludes.php
index 2f47b48e39..f9fd7c99dd 100644
--- a/Sources/ServerSideIncludes.php
+++ b/Sources/ServerSideIncludes.php
@@ -522,13 +522,19 @@ public static function queryPosts(
$row['body'] = Autolinker::load(true)->makeLinks($row['body']);
}
- $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], $row['id_msg']);
+ $row['body'] = Parser::transform(
+ string: $row['body'],
+ input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0),
+ options: ['cache_id' => (int) $row['id_msg']],
+ );
+
+ $row['body'] = strtr($row['body'], [Utils::TAB_SUBSTITUTE => '' . "\t" . ' ']);
// Censor it!
Lang::censorText($row['subject']);
Lang::censorText($row['body']);
- $preview = strip_tags(strtr($row['body'], [' ' => '
']));
+ $preview = strip_tags(strtr($row['body'], [' ' => '
', '' => '', '
' => '
']));
// Build the array.
$posts[$row['id_msg']] = [
@@ -702,7 +708,18 @@ public static function recentTopics(int $num_recent = 8, ?array $exclude_boards
$posts = [];
while ($row = Db::$db->fetch_assoc($request)) {
- $row['body'] = strip_tags(strtr(BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], $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),
+ output_type: Parser::OUTPUT_TEXT,
+ options: [
+ 'cache_id' => (int) $row['id_msg'],
+ 'str_replace' => [
+ ' ' => '
',
+ Utils::TAB_SUBSTITUTE => ' ',
+ ],
+ ],
+ );
if (Utils::entityStrlen($row['body']) > 128) {
$row['body'] = Utils::entitySubstr($row['body'], 0, 128) . '...';
@@ -2239,7 +2256,13 @@ public static function boardNews(?int $board = null, ?int $limit = null, ?int $s
$row['body'] .= '...';
}
- $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], $row['id_msg']);
+ $row['body'] = Parser::transform(
+ string: $row['body'],
+ input_types: Parser::INPUT_BBC | Parser::INPUT_MARKDOWN | ((bool) $row['smileys_enabled'] ? Parser::INPUT_SMILEYS : 0),
+ options: ['cache_id' => (int) $row['id_msg']],
+ );
+
+ $row['body'] = strtr($row['body'], [Utils::TAB_SUBSTITUTE => '' . "\t" . ' ']);
if (!empty($recycle_board) && $row['id_board'] == $recycle_board) {
$row['icon'] = 'recycled';
@@ -2308,7 +2331,7 @@ public static function boardNews(?int $board = null, ?int $limit = null, ?int $s
', $news['subject'], '
', $news['time'], ' ', Lang::$txt['by'], ' ', $news['poster']['link'], '
- ', $news['body'], '
+ ', Utils::adjustHeadingLevels($news['body'], 3), '
', $news['link'], $news['locked'] ? '' : ' | ' . $news['comment_link'], '';
// Is there any likes to show?
diff --git a/Sources/Subs-Compat.php b/Sources/Subs-Compat.php
index bca208ddbf..bd3151ba23 100644
--- a/Sources/Subs-Compat.php
+++ b/Sources/Subs-Compat.php
@@ -57,11 +57,9 @@ function Activate()
}
/**
- * End
- * Actions\Activate
* Begin
* Actions\Admin\ACP
- * */
+ */
function AdminMain()
{
return Actions\Admin\ACP::call();
@@ -108,9 +106,6 @@ function adminLogin(string $type = 'admin'): void
}
/**
- * End
- * Actions\Admin\ACP
- *
* Begin
* Actions\Admin\AntiSpam
*/
@@ -120,9 +115,6 @@ function ModifyAntispamSettings(bool $return_config = false): ?array
}
/**
- * End
- * Actions\Admin\AntiSpam
- *
* Begin
* Actions\Admin\Attachments
*/
@@ -192,9 +184,6 @@ function TransferAttachments(): void
}
/**
- * End
- * Actions\Admin\Attachments
- *
* Begin
* Actions\Admin\Bans
*/
@@ -234,9 +223,6 @@ function BanLog(): void
}
/**
- * End
- * Actions\Admin\Bans
- *
* Begin
* Actions\Admin\Boards
*/
@@ -251,9 +237,6 @@ function EditBoardSettings(bool $return_config = false): ?array
}
/**
- * End
- * Actions\Admin\Boards
- *
* Begin
* Actions\Admin\Calendar
*/
@@ -278,9 +261,6 @@ function ModifyCalendarSettings(bool $return_config = false): void
}
/**
- * End
- * Actions\Admin\Calendar
- *
* Begin
* Actions\Admin\EndSession
*/
@@ -290,9 +270,6 @@ function AdminEndSession(): void
}
/**
- * End
- * Actions\Admin\EndSession
- *
* Begin
* Actions\Admin\ErrorLog
*/
@@ -302,9 +279,6 @@ function ViewErrorLog(): void
}
/**
- * End
- * Actions\Admin\ErrorLog
- *
* Begin
* Actions\Admin\Features
*/
@@ -359,9 +333,6 @@ function ModifyAlertsSettings(): void
}
/**
- * End
- * Actions\Admin\Features
- *
* Begin
* Actions\Admin\Find
*/
@@ -371,9 +342,6 @@ function AdminSearch(): void
}
/**
- * End
- * Actions\Admin\Find
- *
* Begin
* Actions\Admin\Home
*/
@@ -383,9 +351,6 @@ function AdminHome(): void
}
/**
- * End
- * Actions\Admin\Home
- *
* Begin
* Actions\Admin\Languages
*/
@@ -420,9 +385,6 @@ function ModifyLanguage(): void
}
/**
- * End
- * Actions\Admin\Languages
- *
* Begin
* Actions\Admin\Logs
*/
@@ -432,9 +394,6 @@ function AdminLogs(bool $return_config = false): ?array
}
/**
- * End
- * Actions\Admin\Logs
- *
* Begin
* Actions\Admin\Mail
*/
@@ -469,9 +428,6 @@ function TestMailSend(): void
}
/**
- * End
- * Actions\Admin\Mail
- *
* Begin
* Actions\Admin\Maintenance
*/
@@ -608,9 +564,6 @@ function MaintainRemoveOldDrafts(): void
}
/**
- * End
- * Actions\Admin\Maintainence
- *
* Begin
* Actions\Admin\Membergroups
*/
@@ -645,9 +598,6 @@ function ModifyMembergroupsettings(bool $return_config = false): ?array
}
/**
- * End
- * Actions\Admin\Membergoups
- *
* Begin
* Actions\Admin\Members
*/
@@ -677,9 +627,6 @@ function SearchMembers(): void
}
/**
- * End
- * Actions\Admin\Members
- *
* Begin
* Actions\Admin\Mods
*/
@@ -689,9 +636,6 @@ function ModifyModSettings(bool $return_config = false): ?array
}
/**
- * End
- * Actions\Admin\Mods
- *
* Begin
* Actions\Admin\News
*/
@@ -731,9 +675,6 @@ function ModifyNewsSettings(bool $return_config = false): ?array
}
/**
- * End
- * Actions\Admin\News
- *
* Begin
* Actions\Admin\Permissions
*/
@@ -828,9 +769,6 @@ function GeneralPermissionSettings(bool $return_config = false): ?array
}
/**
- * End
- * Actions\Admin\Permissions
- *
* Begin
* Actions\Admin\Post
*/
@@ -855,9 +793,6 @@ function ModifyDraftSettings(bool $return_config = false): ?array
}
/**
- * End
- * Actions\Admin\Posts
- *
* Begin
* Actions\Admin\Registration
*/
@@ -892,9 +827,6 @@ function ModifyRegistrationSettings(bool $return_config = false): ?array
}
/**
- * End
- * Actions\Admin\Registration
- *
* Begin
* Actions\Admin\RepairBoards
*/
@@ -904,9 +836,6 @@ function RepairBoards(): void
}
/**
- * End
- * Actions\Admin\RepairBoards
- *
* Begin
* Actions\Admin\Reports
*/
@@ -941,9 +870,6 @@ function StaffReport(): void
}
/**
- * End
- * Actions\Admin\Reports
- *
* Begin
* Actions\Admin\Search
*/
@@ -973,9 +899,6 @@ function CreateMessageIndex(): void
}
/**
- * End
- * Actions\Admin\Search
- *
* Begin
* Actions\Admin\SearchEngines
*/
@@ -1020,9 +943,6 @@ function EditSpider(): void
}
/**
- * End
- * Actions\Admin\SearchEngine
- *
* Begin
* Actions\Admin\Server
*/
@@ -1087,9 +1007,6 @@ function ShowPHPinfoSettings(): void
}
/**
- * End
- * Actions\Admin\Server
- *
* Begin
* Actions\Admin\Smileys
*/
@@ -1129,9 +1046,6 @@ function EditMessageIcons(): void
}
/**
- * End
- * Actions\Admin\Smileys
- *
* Begin
* Actions\Admin\Subscriptions
*/
@@ -1196,9 +1110,6 @@ function ModifySubscriptionSettings(bool $return_config = false): ?array
}
/**
- * End
- * Actions\Admin\Subscriptions
- *
* Begin
* Actions\Admin\Task
*/
@@ -1228,9 +1139,6 @@ function TaskSettings(bool $return_config = false): ?array
}
/**
- * End
- * Actions\Admin\Tasks
- *
* Begin
* Actions\Admin\Themes
*/
@@ -1280,9 +1188,6 @@ function CopyTemplate(): void
}
/**
- * End
- * Actions\Admin\Themes
- *
* Begin
* Actions\Admin\Warnings
*/
@@ -1292,9 +1197,6 @@ function ModifyWarningSettings(bool $return_config = false): ?array
}
/**
- * End
- * Actions\Admin\Warnings
- *
* Begin
* Actions\Moderation\EndSession
*/
@@ -1304,9 +1206,6 @@ function ModEndSession(): void
}
/**
- * End
- * Actions\Moderation\EndSession
- *
* Begin
* Actions\Moderation\Home
*/
@@ -1316,9 +1215,6 @@ function ModerationHome(): void
}
/**
- * End
- * Actions\Moderation\Home
- *
* Begin
* Actions\Moderation\Logs
*/
@@ -1328,9 +1224,6 @@ function ViewModlog(): void
}
/**
- * End
- * Actions\Moderation\Logs
- *
* Begin
* Actions\Moderation\Main
*/
@@ -1345,9 +1238,6 @@ function ModerationMail(bool $dont_call = false): void
}
/**
- * End
- * Actions\Moderation\Main
- *
* Begin
* Actions\Moderation\Posts
*/
@@ -1377,9 +1267,6 @@ function ApproveMessage(): void
}
/**
- * End
- * Actions\Moderation\Posts
- *
* Begin
* Actions\Moderation\ReportedContent
*/
@@ -1424,9 +1311,6 @@ function EditComment(): void
}
/**
- * End
- * Actions\Moderation\ReportedContent
- *
* Begin
* Actions\Moderation\ShowNotice
*/
@@ -1436,9 +1320,6 @@ function ShowNotice(): void
}
/**
- * End
- * Actions\Moderation\ShowNotice
- *
* Begin
* Actions\Moderation\Warnings
*/
@@ -1463,9 +1344,6 @@ function ModifyWarningTemplate(): void
}
/**
- * End
- * Actions\Moderation\Warnings
- *
* Begin
* Actions\Moderation\WatchedUsers
*/
@@ -1475,9 +1353,6 @@ function ViewWatchedUsers(): void
}
/**
- * End
- * Actions\Moderation\WatchedUsers
- *
* Begin
* Actions\Profile\Account
*/
@@ -1487,9 +1362,6 @@ function account(): void
}
/**
- * End
- * Actions\Profile\Account
- *
* Begin
* Actions\Profile\Activate
*/
@@ -1499,9 +1371,6 @@ function activateAccount(): void
}
/**
- * End
- * Actions\Profile\Activate
- *
* Begin
* Actions\Profile\AlertsPopup
*/
@@ -1511,9 +1380,6 @@ function alerts_popup(): void
}
/**
- * End
- * Actions\Profile\AlertsPopup
- *
* Begin
* Actions\Profile\BuddyIgnoreLists
*/
@@ -1543,9 +1409,6 @@ function editIgnoreList(int $memID): void
}
/**
- * End
- * Actions\Profile\BuddyIgnoreLists
- *
* Begin
* Actions\Profile\Delete
*/
@@ -1560,9 +1423,6 @@ function deleteAccount2(int $memID): void
}
/**
- * End
- * Actions\Profile\Delete
- *
* Begin
* Actions\Profile\Export
*/
@@ -1582,9 +1442,6 @@ function get_export_formats(): array
}
/**
- * End
- * Actions\Profile\Export
- *
* Begin
* Actions\Profile\ExportAttachment
*/
@@ -1594,9 +1451,6 @@ function export_attachment(): void
}
/**
- * End
- * Actions\Profile\ExportAttachment
- *
* Begin
* Actions\Profile\ExportDownload
*/
@@ -1606,9 +1460,6 @@ function download_export_file(): void
}
/**
- * End
- * Actions\Profile\ExportDownload
- *
* Begin
* Actions\Profile\ForumProfile
*/
@@ -1618,9 +1469,6 @@ function forumProfile(): void
}
/**
- * End
- * Actions\Profile\ForumProfile
- *
* Begin
* Actions\Profile\GroupMembership
*/
@@ -1635,9 +1483,6 @@ function groupMembership2(int $memID): string
}
/**
- * End
- * Actions\Profile\GroupMembership
- *
* Begin
* Actions\Profile\IgnoreBoards
*/
@@ -1647,9 +1492,6 @@ function ignoreboards(): void
}
/**
- * End
- * Actions\Profile\IgnoreBoards
- *
* Begin
* Actions\Profile\IssueWarning
*/
@@ -1662,9 +1504,6 @@ function issueWarning(int $memID): void
}
/**
- * End
- * Actions\Profile\IssueWarning
- *
* Begin
* Actions\Profile\Main
*/
@@ -1674,9 +1513,6 @@ function ModifyProfile(): void
}
/**
- * End
- * Actions\Profile\Main
- *
* Begin
* Actions\Profile\Notification
*/
@@ -1728,9 +1564,6 @@ function makeNotificationChanges(int $memID): void
}
/**
- * End
- * Actions\Profile\Notification
- *
* Begin
* Actions\Profile\PaidSubs
*/
@@ -1740,9 +1573,6 @@ function subscriptions(): void
}
/**
- * End
- * Actions\Profile\PaidSubs
- *
* Begin
* Actions\Profile\Popup
*/
@@ -1752,9 +1582,6 @@ function profile_popup(): void
}
/**
- * End
- * Actions\Profile\Popup
- *
* Begin
* Actions\Profile\ShowAlerts
*/
@@ -1767,9 +1594,6 @@ function showAlerts(int $memID): void
}
/**
- * End
- * Actions\Profile\ShowAlerts
- *
* Begin
* Actions\Profile\ShowPermissions
*/
@@ -1782,9 +1606,6 @@ function showPermissions(int $memID): void
}
/**
- * End
- * Actions\Profile\ShowPermissions
- *
* Begin
* Actions\Profile\ShowPost
*/
@@ -1804,9 +1625,6 @@ function showAttachments(int $memID): void
}
/**
- * End
- * Actions\Profile\ShowPosts
- *
* Begin
* Actions\Profile\StatPanel
*/
@@ -1816,9 +1634,6 @@ function statPanel(int $memID): void
}
/**
- * End
- * Actions\Profile\StatPanel
- *
* Begin
* Actions\Profile\Summary
*/
@@ -1828,9 +1643,6 @@ function summary(int $memID): void
}
/**
- * End
- * Actions\Profile\Summary
- *
* Begin
* Actions\Profile\TFADisable
*/
@@ -1840,9 +1652,6 @@ function tfadisable(): void
}
/**
- * End
- * Actions\Profile\TFDADisable
- *
* Begin
* Actions\Profile\TFASetup
*/
@@ -1852,9 +1661,6 @@ function tfasetup(): void
}
/**
- * End
- * Actions\Profile\TFASetup
- *
* Begin
* Actions\Profile\ThemeOptions
*/
@@ -1864,9 +1670,6 @@ function theme(): void
}
/**
- * End
- * Actions\Profile\ThemeOptions
- *
* Begin
* Actions\Profile\Tracking
*/
@@ -1900,9 +1703,6 @@ function TrackLogins(int $memID): void
}
/**
- * End
- * Actions\Profile\Tracking
- *
* Begin
* Actions\Profile\ViewWarning
*/
@@ -1912,9 +1712,6 @@ function viewWarning(int $memID): void
}
/**
- * End
- * Actions\Profile\ViewWarning
- *
* Begin
* Actions\Agreement
*/
@@ -1934,9 +1731,6 @@ function canRequirePrivacyPolicy(): bool
}
/**
- * End
- * Actions\Agreement
- *
* Begin
* Actions\AgreementAccept
*/
@@ -1946,9 +1740,6 @@ function AcceptAgreement(): void
}
/**
- * End
- * Actions\AgreementAccept
- *
* Begin
* Actions\Announce
*/
@@ -1968,9 +1759,6 @@ function AnnouncementSend(): void
}
/**
- * End
- * Actions\Announce
- *
* Begin
* Actions\AttachmentApprove
*/
@@ -1980,9 +1768,6 @@ function ApproveAttach(): void
}
/**
- * End
- * Actions\AttachmentApprove
- *
* Begin
* Actions\AttachmentDownload
*/
@@ -1992,9 +1777,6 @@ function showAttachment(): void
}
/**
- * End
- * Actions\AttachementDownload
- *
* Begin
* Actions\AutoSuggest
*/
@@ -2023,9 +1805,6 @@ function AutoSuggest_Search_SMFVersions(): void
}
/**
- * End
- * Actions\AutoSuggest
- *
* Begin
* Actions\BoardIndex
*/
@@ -2045,9 +1824,6 @@ function getBoardIndex(array $board_index_options): array
}
/**
- * End
- * Actions\BoardIndex
- *
* Begin
* Actions\BuddyListToggle
*/
@@ -2057,9 +1833,6 @@ function BuddyListToggle(): void
}
/**
- * End
- * Actions\BuddyListToggle
- *
* Begin
* Actions\Calendar
*/
@@ -2173,9 +1946,6 @@ function convertDateToEnglish(string $date): string
}
/**
- * End
- * Actions\Calendar
- *
* Begin
* Actions\CoppaForm
*/
@@ -2185,9 +1955,6 @@ function CoppaForm(): void
}
/**
- * End
- * Actions\CoppaForm
- *
* Begin
* Actions\Credits
*/
@@ -2197,9 +1964,6 @@ function Credits(bool $in_admin = false): void
}
/**
- * End
- * Actions\Credits
- *
* Begin
* Actions\Display
*/
@@ -2209,9 +1973,6 @@ function Display(): void
}
/**
- * End
- * Actions\Display
- *
* Begin
* Actions\DisplayAdminFile
*/
@@ -2221,9 +1982,6 @@ function DisplayAdminFile(): void
}
/**
- * End
- * Actions\DisplayAdminFile
- *
* Begin
* Actions\Feed
*/
@@ -2243,9 +2001,6 @@ function cdata_parse(string $data, string $ns = '', bool $force = false): string
}
/**
- * End
- * Actions\Feed
- *
* Begin
* Actions\FindMember
* @deprecated
@@ -2256,9 +2011,6 @@ function JSMembers(): void
}
/**
- * End
- * Actions\FindMember
- *
* Begin
* Actions\Groups
*/
@@ -2288,9 +2040,6 @@ function GroupRequests(): void
}
/**
- * End
- * Actions\Groups
- *
* Begin
* Actions\Help
*/
@@ -2305,9 +2054,6 @@ function HelpIndex(): void
}
/**
- * End
- * Actions\Help
- *
* Begin
* Actions\HelpAdmin
*/
@@ -2317,9 +2063,6 @@ function ShowAdminHelp(): void
}
/**
- * End
- * Actions\HelpAdmin
- *
* Begin
* Actions\JavaScriptModify
*/
@@ -2329,9 +2072,6 @@ function JavaScriptModify(): void
}
/**
- * End
- * Actions\JavaScriptModify
- *
* Begin
* Actions\Login
*/
@@ -2341,9 +2081,6 @@ function Login(): void
}
/**
- * End
- * Actions\Login
- *
* Begin
* Actions\Login2
*/
@@ -2374,9 +2111,6 @@ function validatePasswordFlood(
}
/**
- * End
- * Actions\Login2
- *
* Begin
* Actions\LoginTFA
*/
@@ -2386,9 +2120,6 @@ function LoginTFA(): void
}
/**
- * End
- * Actions\LoginTFA
- *
* Begin
* Actions\Logout
*/
@@ -2398,9 +2129,6 @@ function Logout(): void
}
/**
- * End
- * Actions\Logout
- *
* Begin
* Actions\Memberlist
*/
@@ -2430,9 +2158,6 @@ function getCustFieldsMList(): array
}
/**
- * End
- * Actions\Memberlist
- *
* Begin
* Actions\MessageIndex
*/
@@ -2452,9 +2177,6 @@ function buildTopicContext(array $row): void
}
/**
- * End
- * Actions\MessageIndex
- *
* Begin
* Actions\MsgDelete
*/
@@ -2464,9 +2186,6 @@ function DeleteMessage(): void
}
/**
- * End
- * Actions\MsgDelete
- *
* Begin
* Actions\Notify
*/
@@ -2496,9 +2215,6 @@ function createUnsubscribeToken(int $memID, string $email, string $type = '', in
}
/**
- * End
- * Actions\Notify
- *
* Begin
* Actions\NotifyAnnouncements
*/
@@ -2508,9 +2224,6 @@ function AnnouncementsNotify(): void
}
/**
- * End
- * Actions\NotifyAnnouncements
- *
* Begin
* Actions\NotifyBoard
*/
@@ -2520,9 +2233,6 @@ function BoardNotify(): void
}
/**
- * End
- * Actions\NotifyBoard
- *
* Begin
* Actions\NotifyTopic
*/
@@ -2532,9 +2242,6 @@ function TopicNotify(): void
}
/**
- * End
- * Actions\NotifyTopic
- *
* Begin
* Actions\PersonalMessage
*/
@@ -2614,9 +2321,6 @@ function MessageDrafts(): void
}
/**
- * End
- * Actions\PersonalMessage
- *
* Begin
* Actions\Post
*/
@@ -2626,9 +2330,6 @@ function Post(): void
}
/**
- * End
- * Actions\Post
- *
* Begin
* Actions\Post2
*/
@@ -2638,9 +2339,6 @@ function Post2(): void
}
/**
- * End
- * Actions\Post2
- *
* Begin
* Actions\QuickModeration
*/
@@ -2650,9 +2348,6 @@ function QuickModeration(): void
}
/**
- * End
- * Actions\QuickModeration
- *
* Begin
* Actions\QuickModerationInTopic
*/
@@ -2662,9 +2357,6 @@ function QuickInTopicModeration(): void
}
/**
- * End
- * Actions\QuickModerationInTopic
- *
* Begin
* Actions\QuoteFast
*/
@@ -2674,9 +2366,6 @@ function QuoteFast(): void
}
/**
- * End
- * Actions\QuoteFast
- *
* Begin
* Actions\Recent
*/
@@ -2691,9 +2380,6 @@ function getLastPost(): array
}
/**
- * End
- * Actions\Recent
- *
* Begin
* Actions\Register
*/
@@ -2703,9 +2389,6 @@ function Register(array $reg_errors = []): void
}
/**
- * End
- * Actions\Register
- *
* Begin
* Actions\Register2
*/
@@ -2720,9 +2403,6 @@ function registerMember(array &$reg_options, bool $return_errors = false): int|a
}
/**
- * End
- * Actions\Register2
- *
* Begin
* Actions\Reminder
*/
@@ -2732,9 +2412,6 @@ function RemindMe(): void
}
/**
- * End
- * Actions\Reminder
- *
* Begin
* Actions\ReportToMod
*/
@@ -2759,9 +2436,6 @@ function reportUser($id_member, $reason): void
}
/**
- * End
- * Actions\ReportToMod
- *
* Begin
* Actions\RequestMembers
*/
@@ -2771,9 +2445,6 @@ function RequestMembers(): void
}
/**
- * End
- * Actions\RequestMembers
- *
* Begin
* Actions\Search
*/
@@ -2783,9 +2454,6 @@ function PlushSearch1(): void
}
/**
- * End
- * Actions\Search
- *
* Begin
* Actions\Search2
*/
@@ -2795,9 +2463,6 @@ function PlushSearch2(): void
}
/**
- * End
- * Actions\Search2
- *
* Begin
* Actions\SendActivation
*/
@@ -2807,9 +2472,6 @@ function SendActivation(): void
}
/**
- * End
- * Actions\SendActivation
- *
* Begin
* Actions\SmStats
*/
@@ -2819,9 +2481,6 @@ function SMStats(): void
}
/**
- * End
- * Actions\SmStats
- *
* Begin
* Actions\Stats
*/
@@ -2831,9 +2490,6 @@ function DisplayStats(): void
}
/**
- * End
- * Actions\Stats
- *
* Begin
* Actions\TopicMerge
*/
@@ -2858,9 +2514,6 @@ function MergeDone(): void
}
/**
- * End
- * Actions\TopicMerge
- *
* Begin
* Actions\TopicMove
*/
@@ -2870,9 +2523,6 @@ function MoveTopic(): void
}
/**
- * End
- * Actions\TopicMove
- *
* Begin
* Actions\TopicMove2
*/
@@ -2887,9 +2537,6 @@ function moveTopicConcurrence()
}
/**
- * End
- * Actions\TopicMove2
- *
* Begin
* Actions\TopicPrint
*/
@@ -2899,9 +2546,6 @@ function PrintTopic(): void
}
/**
- * End
- * Actions\TopicPrint
- *
* Begin
* Actions\TopicRemove
*/
@@ -2921,9 +2565,6 @@ function RemoveOldTopics2()
}
/**
- * End
- * Actions\TopicRemove
- *
* Begin
* Actions\TopicRestore
*/
@@ -2933,9 +2574,6 @@ function RestoreTopic(): void
}
/**
- * End
- * Actions\TopicRestore
- *
* Begin
* Actions\TopicSplit
*/
@@ -2970,9 +2608,6 @@ function SplitSelectionExecute(): void
}
/**
- * End
- * Actions\TopicSplit
- *
* Begin
* Actions\TrackIP
*/
@@ -2983,9 +2618,6 @@ function TrackIP(int $memID = 0): void
}
/**
- * End
- * Actions\TrackIP
- *
* Begin
* Actions\Unread
*/
@@ -2995,9 +2627,6 @@ function UnreadTopics(): void
}
/**
- * End
- * Actions\Unread
- *
* Begin
* Actions\VerificationCode
*/
@@ -3007,9 +2636,6 @@ function VerificationCode(): void
}
/**
- * End
- * Actions\VerificationCode
- *
* Begin
* Actions\ViewQUery
*/
@@ -3019,9 +2645,6 @@ function ViewQuery(): void
}
/**
- * End
- * Actions\ViewQUery
- *
* Begin
* Actions\Who
*/
@@ -3036,9 +2659,6 @@ function determineActions(string|array $urls, string|bool $preferred_prefix = fa
}
/**
- * End
- * Actions\Who
- *
* Begin
* Actions\XmlHttp
*/
@@ -3063,10 +2683,6 @@ function RetrievePreview(): void
}
/**
- * End
- * Actions\XmlHttp
- * End Actions\*
- *
* Begin
* Cache\CacheApi
*/
@@ -3101,9 +2717,6 @@ function cache_get_data(string $key, int $ttl = 120): mixed
}
/**
- * End
- * Cache\CacheApi
- *
* Begin
* Db\DatabaseApi
*/
@@ -3118,9 +2731,6 @@ function db_extend()
}
/**
- * End
- * Db\DatabaseApi
- *
* Begin
* Graphics\Image
*/
@@ -3211,9 +2821,6 @@ function resizeImage(
}
/**
- * End
- * Graphics\Image
- *
* Begin
* Packagemanager\SubsPackage
*/
@@ -3413,9 +3020,6 @@ function package_validate_send(array $sendData): array
}
/**
- * End
- * PackageManager\SubsPackage
- *
* Begin
* PersonalMessage\DraftPM
*/
@@ -3430,9 +3034,6 @@ function showInProfile(int $memID = -1): void
}
/**
- * End
- * PersonalMessage\DraftPM
- *
* Begin
* PersonalMessage\PM
*/
@@ -3498,9 +3099,6 @@ function isAccessiblePM(int $pmID, string $folders = 'both'): bool
}
/**
- * End
- * PersonalMessage\PM
- *
* Begin
* PersonalMessage\Rule
*/
@@ -3525,9 +3123,6 @@ function manage(): void
}
/**
- * End
- * PersonalMessage\Rule
- *
* Begin
* Search\SearchApi
*/
@@ -3542,9 +3137,6 @@ function loadSearchAPIs(): array
}
/**
- * End
- * Search\SearchApi
- *
* Begin
* Search\SearchResult
*/
@@ -3554,9 +3146,6 @@ function highlight(string $text, array $words): string
}
/**
- * End
- * Search\SearchResult
- *
* Begin
* Unicode\Utf8String
* @see SMF\BackwardCompatibility
@@ -3627,9 +3216,6 @@ function utf8_sanitize_invisibles(string $string, int $level, string $substitute
}
/**
- * End
- * Unicode\Utf8String
- *
* Begin
* WebFetch\WebFetchApi
*/
@@ -3639,9 +3225,6 @@ function fetch_web_data(string $url, string|array $post_data = [], bool $keep_al
}
/**
- * End
- * WebFetch\WebFetchApi
- *
* Begin
* SMF\Alert
*/
@@ -3681,9 +3264,6 @@ function alert_purge(int $memID = 0, int $before = 0): void
}
/**
- * End
- * SMF\Alert
- *
* Begin
* SMF\Attachment
*/
@@ -3788,50 +3368,61 @@ function getAttachmentFilename(
}
/**
- * End
- * SMF\Attachment
- *
* Begin
- * SMF\BBCodeParser
+ * SMF\Parsers\BBCodeParser
*/
function get_signature_allowed_bbc_tags(): array
{
- return SMF\BBCodeParser::getSigTags();
+ return SMF\Parser::getSigTags();
}
function highlight_php_code(string $code): string
{
- return SMF\BBCodeParser::highlightPhpCode($code);
+ return SMF\Parser::highlightPhpCode($code);
}
function sanitizeMSCutPaste(string $string): string
{
- return SMF\BBCodeParser::sanitizeMSCutPaste($string);
+ return SMF\Parser::sanitizeMSCutPaste($string);
}
function parse_bbc(
string|bool $message,
- bool $smileys = true,
+ bool|string $smileys = true,
string $cache_id = '',
array $parse_tags = [],
): string|array {
- return SMF\BBCodeParser::backcompatParseBbc(
- $message,
- $smileys,
- $cache_id,
- $parse_tags,
+ if ($message === false) {
+ return SMF\Parser::getBBCodes();
+ }
+
+ return SMF\Parser::transform(
+ string: $message,
+ input_types: SMF\Parser::INPUT_BBC | SMF\Parser::INPUT_MARKDOWN | (!empty($smileys) ? SMF\Parser::INPUT_SMILEYS : 0),
+ options: [
+ 'cache_id' => $cache_id,
+ 'parse_tags' => $parse_tags,
+ 'for_print' => $smileys === 'print',
+ ],
);
}
function parseSmileys(string &$message): void
{
- SMF\BBCodeParser::backcompatParseSmileys($message);
+ $message = SMF\Parser::transform($message, SMF\Parser::INPUT_SMILEYS);
+ }
+
+ function html_to_bbc(string $string): string
+ {
+ // We want to ignore Markdown in this backward compatibility function.
+ return SMF\Parser::transform(
+ string: $string,
+ input_types: SMF\Parser::INPUT_BBC | SMF\Parser::INPUT_SMILEYS,
+ output_type: SMF\Parser::OUTPUT_BBC,
+ );
}
/**
- * End
- * SMF\BBCodeParser
- *
* Begin
* SMF\Board
*/
@@ -3906,9 +3497,6 @@ function getBoardParents(int $id_parent): array
}
/**
- * End
- * SMF\Board
- *
* Begin
* SMF\BrowserDetector
*/
@@ -3923,9 +3511,6 @@ function isBrowser(string $browser): bool
}
/**
- * End
- * SMF\BrowserDetector
- *
* Begin
* SMF\Category
*/
@@ -3965,9 +3550,6 @@ function recursiveBoards(&$list, &$tree): void
}
/**
- * End
- * SMF\Category
- *
* Begin
* SMF\Cookie
*/
@@ -4014,9 +3596,6 @@ function smf_setcookie(
}
/**
- * End
- * SMF\Cookie
- *
* Begin
* SMF\Draft
*/
@@ -4036,9 +3615,6 @@ function showProfileDrafts(int $memID): void
}
/**
- * End
- * SMF\Draft
- *
* Begin
* SMF\Editor
*/
@@ -4053,9 +3629,6 @@ function getMessageIcons(int $board_id): array
}
/**
- * End
- * SMF\Editor
- *
* Begin
* SMF\ErrorHandler
*/
@@ -4104,9 +3677,6 @@ function display_loadavg_error(): void
}
/**
- * End
- * SMF\ErrorHandler
- *
* Begin
* SMF\Event
*/
@@ -4126,9 +3696,6 @@ function removeEvent(int $id): void
}
/**
- * End
- * SMF\Event
- *
* Begin
* SMF\Group
*/
@@ -4170,9 +3737,6 @@ function cache_getMembergroupList(): array
}
/**
- * End
- * SMF\Group
- *
* Begin
* SMF\IntegrationHook
*/
@@ -4214,9 +3778,6 @@ function remove_integration_function(
}
/**
- * End
- * SMF\IntegrationHook
- *
* Begin
* SMF\IP
*/
@@ -4267,9 +3828,6 @@ function expandIPv6(string $ip, bool $return_bool_if_invalid = true): string|boo
}
/**
- * End
- * SMF\IP
- *
* Begin
* SMF\ItemList
*/
@@ -4279,9 +3837,6 @@ function createList(array $options): SMF\ItemList
}
/**
- * End
- * SMF\ItemList
- *
* Begin
* SMF\Lang
*/
@@ -4320,9 +3875,6 @@ function comma_format(int|float $number, ?int $decimals = null): string
}
/**
- * End
- * SMF\Lang
- *
* Begin
* SMF\Logging
*/
@@ -4367,9 +3919,6 @@ function displayDebug(): void
}
/**
- * End
- * SMF\Logging
- *
* Begin
* SMF\Mail
*/
@@ -4470,9 +4019,6 @@ function loadEmailTemplate(
}
/**
- * End
- * SMF\Mail
- *
* Begin
* SMF\Menu
*/
@@ -4487,9 +4033,6 @@ function destroyMenu(int|string $id = 'last'): void
}
/**
- * End
- * SMF\Menu
- *
* Begin
* SMF\Msg
*/
@@ -4557,9 +4100,6 @@ function removeMessage(int $message, bool $decreasePostCount = true): bool
}
/**
- * End
- * SMF\Msg
- *
* Begin
* SMF\PageIndex
*/
@@ -4582,9 +4122,6 @@ function constructPageIndex(
}
/**
- * End
- * SMF\PageIndex
- *
* Begin
* SMF\Poll
*/
@@ -4619,9 +4156,6 @@ function RemovePoll(): void
}
/**
- * End
- * SMF\Poll
- *
* Begin
* SMF\Profile
*/
@@ -4710,9 +4244,6 @@ function makeThemeChanges(int $id, int $id_theme): void
}
/**
- * End
- * SMF\Profile
- *
* Begin
* SMF\QueryString
*/
@@ -4737,9 +4268,6 @@ function matchIPtoCIDR(string $ip_address, string $cidr_address): bool
}
/**
- * End
- * SMF\QueryString
- *
* Begin
* SMF\Sapi
*/
@@ -4752,9 +4280,6 @@ function memoryReturnBytes(string $val): int
return Sapi::memoryReturnBytes($val);
}
/**
- * End
- * SMF\Sapi
- *
* Begin
* SMF\Security
*/
@@ -4809,9 +4334,6 @@ function KickGuest(): void
}
/**
- * End
- * SMF\Security
- *
* Begin
* SMF\SecurityToken
*/
@@ -4831,9 +4353,6 @@ function cleanTokens(bool $complete = false): void
}
/**
- * End
- * SMF\SecurityToken
- *
* BEgin
* SMF\ServerSideIncludes
*/
@@ -5093,9 +4612,6 @@ function ssi_recentAttachments(int $num_attachments = 10, array $attachment_ext
}
/**
- * End
- * SMF\ServerSideIncludes
- *
* Begin
* SMF\Session
*/
@@ -5105,9 +4621,6 @@ function loadSession(): void
}
/**
- * End
- * SMF\Session
- *
* Begin
* SMF\TaskRunner
*/
@@ -5117,9 +4630,6 @@ function CalculateNextTrigger(string|array $tasks = [], bool $force_update = fal
}
/**
- * End
- * SMF\TaskRunner
- *
* Begin
* SMF\Theme
*/
@@ -5229,9 +4739,6 @@ function PickTheme(): void
}
/**
- * End
- * SMF\Theme
- *
* Begin
* SMF\Time
*/
@@ -5267,9 +4774,6 @@ function forum_time(bool $use_user_offset = true, ?int $timestamp = null): int
}
/**
- * End
- * SMF\Time
- *
* Begin
* SMF\TimeZone
*/
@@ -5299,9 +4803,6 @@ function validate_iso_country_codes(array|string $country_codes, bool $as_csv =
}
/**
- * End
- * SMF\TimeZone
- *
* Begin
* SMF\Topic
*/
@@ -5340,9 +4841,6 @@ function prepareLikesContext(int $topic): array
}
/**
- * End
- * SMF\Topic
- *
* Begin
* SMF\Url
*/
@@ -5412,9 +4910,6 @@ function httpsRedirectActive(string $url): bool
}
/**
- * End
- * SMF\Url
- *
* Begin
* SMF\User
*/
@@ -5574,9 +5069,6 @@ function boardsAllowedTo(string|array $permission, bool $check_access = true, bo
}
/**
- * End
- * SMF\User
- *
* Begin
* SMF\Utils
*/
@@ -5771,9 +5263,6 @@ function entity_fix__callback(array $matches): string
}
/**
- * End
- * SMF\Utils
- *
* Begin
* SMF\Verifier
*/
@@ -5781,11 +5270,6 @@ function create_control_verification(array &$options, bool $do_test = false): bo
{
return SMF\Verifier::create($options, $do_test);
}
-
- /*
- * End
- * BackwardCompatibility function map
- */
}
/***************************
diff --git a/Sources/Tasks/CreatePost_Notify.php b/Sources/Tasks/CreatePost_Notify.php
index 3f73fd9cf6..81cbbec92e 100644
--- a/Sources/Tasks/CreatePost_Notify.php
+++ b/Sources/Tasks/CreatePost_Notify.php
@@ -17,13 +17,13 @@
use SMF\Actions\Notify;
use SMF\Alert;
-use SMF\BBCodeParser;
use SMF\Config;
use SMF\Db\DatabaseApi as Db;
use SMF\ErrorHandler;
use SMF\Lang;
use SMF\Mail;
use SMF\Mentions;
+use SMF\Parser;
use SMF\TaskRunner;
use SMF\Theme;
use SMF\User;
@@ -557,10 +557,11 @@ protected function handleWatchedNotifications(): void
$localization = implode('|', [$member_data['lngfile'], $member_data['time_offset'], $member_data['time_format']]);
if (empty($parsed_message[$localization])) {
- $bbcparser = new BBCodeParser();
- $bbcparser->time_offset = $member_data['time_offset'];
- $bbcparser->time_format = $member_data['time_format'];
- $bbcparser->smiley_set = $member_data['smiley_set'];
+ // Use the target member's localization settings.
+ Parser::$time_offset = $member_data['time_offset'];
+ Parser::$time_format = $member_data['time_format'];
+ Parser::$smiley_set = $member_data['smiley_set'];
+ Parser::$locale = Lang::$txt['lang_locale'];
$parsed_message[$localization]['subject'] = $msgOptions['subject'];
$parsed_message[$localization]['body'] = $msgOptions['body'];
@@ -569,7 +570,36 @@ protected function handleWatchedNotifications(): void
Lang::censorText($parsed_message[$localization]['body']);
$parsed_message[$localization]['subject'] = Utils::htmlspecialcharsDecode($parsed_message[$localization]['subject']);
- $parsed_message[$localization]['body'] = trim(Utils::htmlspecialcharsDecode(strip_tags(strtr($bbcparser->parse($parsed_message[$localization]['body'], false), [' ' => "\n", '' => "\n", '' => "\n", '[' => '[', ']' => ']', ''' => '\'', '' => "\n", '' => "\t", ' ' => "\n---------------------------------------------------------------\n"]))));
+
+ $parsed_message[$localization]['body'] = strtr(
+ Parser::transform(
+ $parsed_message[$localization]['body'],
+ Parser::INPUT_BBC | Parser::INPUT_MARKDOWN,
+ ),
+ [
+ ' ' => "\n",
+ '' => "\n",
+ '' => "\n",
+ '[' => '[',
+ ']' => ']',
+ ''' => '\'',
+ '' => "\n",
+ '' => "\t",
+ ' ' => "\n" . str_repeat('-', 63) . "\n",
+ ],
+ );
+
+ $parsed_message[$localization]['body'] = trim(Utils::htmlspecialcharsDecode(strip_tags($parsed_message[$localization]['body'])));
+
+ // Go back to the default localization settings.
+ if (!isset(User::$me)) {
+ User::setMe(0);
+ }
+
+ Parser::$time_offset = User::$me->time_offset;
+ Parser::$time_format = User::$me->$time_format;
+ Parser::$smiley_set = (!empty(User::$me->smiley_set) ? User::$me->smiley_set : (!empty(Config::$modSettings['smiley_sets_default']) ? Config::$modSettings['smiley_sets_default'] : 'none'));
+ Parser::$locale = Lang::getLocaleFromLanguageName(User::$me->$language);
}
// Bitwise check: Receiving an alert?
diff --git a/Sources/Tasks/ExportProfileData.php b/Sources/Tasks/ExportProfileData.php
index fa289f6bbc..70d1757feb 100644
--- a/Sources/Tasks/ExportProfileData.php
+++ b/Sources/Tasks/ExportProfileData.php
@@ -912,6 +912,7 @@ public function execute(): bool
// Use some temporary integration hooks to manipulate BBC parsing during export.
$hook_methods = [
+ 'parser_cache' => 'parser_cache',
'pre_parsebbc' => in_array($this->_details['format'], ['HTML', 'XML_XSLT']) ? 'pre_parsebbc_html' : 'pre_parsebbc_xml',
'post_parsebbc' => 'post_parsebbc',
'bbc_codes' => 'bbc_codes',
@@ -1877,16 +1878,23 @@ public static function add_dtd(
]);
}
+ /**
+ * Adds data to the cache key to distinguish parsing for exports from normal
+ * parsing.
+ */
+ public static function parser_cache(array &$cache_key_extras): void
+ {
+ $cache_key_extras[__CLASS__] = 1;
+ }
+
/**
* Adjusts some parse_bbc() parameters for the special case of HTML and
* XML_XSLT exports.
*/
- public static function pre_parsebbc_html(string &$message, bool &$smileys, string &$cache_id, array &$parse_tags, array &$cache_key_extras): void
+ public static function pre_parsebbc_html(string &$message, bool &$smileys, string &$cache_id, array &$parse_tags): void
{
$cache_id = '';
- $cache_key_extras[__CLASS__] = 1;
-
foreach (['smileys_url', 'attachmentThumbnails'] as $var) {
if (isset(Config::$modSettings[$var])) {
self::$real_modSettings[$var] = Config::$modSettings[$var];
@@ -1900,12 +1908,10 @@ public static function pre_parsebbc_html(string &$message, bool &$smileys, strin
/**
* Adjusts some parse_bbc() parameters for the special case of XML exports.
*/
- public static function pre_parsebbc_xml(string &$message, bool &$smileys, string &$cache_id, array &$parse_tags, array &$cache_key_extras): void
+ public static function pre_parsebbc_xml(string &$message, bool &$smileys, string &$cache_id, array &$parse_tags): void
{
$cache_id = '';
- $cache_key_extras[__CLASS__] = 1;
-
$smileys = false;
if (!isset(Config::$modSettings['disabledBBC'])) {
diff --git a/Sources/Theme.php b/Sources/Theme.php
index 4e38a1ea13..5e5db5389f 100644
--- a/Sources/Theme.php
+++ b/Sources/Theme.php
@@ -774,7 +774,12 @@ public static function setupContext(bool $forceload = false): void
}
// Clean it up for presentation ;).
- Utils::$context['news_lines'][$i] = BBCodeParser::load()->parse(stripslashes(trim(Utils::$context['news_lines'][$i])), true, 'news' . $i);
+ Utils::$context['news_lines'][$i] = Parser::transform(
+ string: stripslashes(trim(Utils::$context['news_lines'][$i])),
+ options: ['cache_id' => 'news' . $i],
+ );
+
+ Utils::$context['news_lines'][$i] = Utils::adjustHeadingLevels(Utils::$context['news_lines'][$i], null);
}
if (!empty(Utils::$context['news_lines']) && (!empty(Config::$modSettings['allow_guestAccess']) || User::$me->is_logged)) {
@@ -2690,6 +2695,10 @@ protected function loadCss(): void
// And of course, let's load the default CSS file.
self::loadCSSFile('index.css', ['minimize' => true, 'order_pos' => 1], 'smf_index');
+ if (!empty(Config::$modSettings['enableMarkdown'])) {
+ self::loadCSSFile('markdown.css', ['minimize' => true, 'order_pos' => 2], 'smf_markdown');
+ }
+
// Here is my luvly Responsive CSS
self::loadCSSFile('responsive.css', ['force_current' => false, 'validate' => true, 'minimize' => true, 'order_pos' => 9000], 'smf_responsive');
@@ -3001,7 +3010,7 @@ protected static function templateInclude(string $filename, bool $once = false):
// I know, I know... this is VERY COMPLICATED. Still, it's good.
if (preg_match('~ (\d+) $~i', $error, $match) != 0) {
$data = file($filename);
- $data2 = BBCodeParser::highlightPhpCode(implode('', $data));
+ $data2 = Parser::highlightPhpCode(implode('', $data));
$data2 = preg_split('~\\ ~', $data2);
// Fix the PHP code stuff...
diff --git a/Sources/User.php b/Sources/User.php
index b5652afc60..3695e28e89 100644
--- a/Sources/User.php
+++ b/Sources/User.php
@@ -1218,7 +1218,15 @@ public function format(bool $display_custom_fields = false): array
Lang::censorText($this->formatted['blurb']);
Lang::censorText($this->formatted['signature']);
- $this->formatted['signature'] = BBCodeParser::load()->parse(str_replace(["\n", "\r"], [' ', ''], $this->formatted['signature']), true, 'sig' . $this->id, BBCodeParser::getSigTags());
+ $this->formatted['signature'] = Parser::transform(
+ string: str_replace(["\n", "\r"], [' ', ''], $this->formatted['signature']),
+ options: [
+ 'cache_id' => 'sig' . $this->id,
+ 'parse_tags' => Parser::getSigTags(),
+ ],
+ );
+
+ $this->formatted['signature'] = Utils::adjustHeadingLevels($this->formatted['signature'], null);
}
// Are we also loading the member's custom fields?
@@ -1252,7 +1260,7 @@ public function format(bool $display_custom_fields = false): array
// BBC?
if ($custom['bbc']) {
- $value = BBCodeParser::load()->parse($value);
+ $value = Utils::adjustHeadingLevels(Parser::transform($value), null);
}
// ... or checkbox?
elseif (isset($custom['type']) && $custom['type'] == 'check') {
@@ -3011,7 +3019,7 @@ public static function updateMemberData(int|array|null $members, array $data): v
foreach ($data as $var => $val) {
switch ($var) {
- case 'birthdate':
+ case 'birthdate':
$type = 'date';
break;
diff --git a/Sources/Utils.php b/Sources/Utils.php
index e30c4090f6..93cc2cc00a 100644
--- a/Sources/Utils.php
+++ b/Sources/Utils.php
@@ -222,6 +222,18 @@ class Utils
*/
public const ENT_NBSP = '&(?' . '>nbsp|#(?' . '>x0*A0|0*160));';
+ /**
+ * @var string
+ *
+ * Used to force the browser not to collapse tabs.
+ *
+ * This will normally be replaced in the final output with a real tab
+ * character wrapped in a span with "white-space: pre-wrap" applied to it.
+ * But if this substitute string somehow makes it into the final output,
+ * it will still look like an appropriately sized string of white space.
+ */
+ public const TAB_SUBSTITUTE = "\u{200B}\u{2007}\u{2007}\u{2007}\u{2007}\u{200B}";
+
/**************************
* Public static properties
**************************/
@@ -271,7 +283,7 @@ class Utils
// Even when enabled, they'll only work in old posts and not new ones.
'legacy_bbc' => [
'acronym', 'bdo', 'black', 'blue', 'flash', 'ftp', 'glow',
- 'green', 'move', 'red', 'shadow', 'tt', 'white',
+ 'green', 'move', 'red', 'shadow', 'white',
],
// Define a list of BBC tags that require permissions to use.
'restricted_bbc' => [
@@ -1177,6 +1189,50 @@ function ($k1, $k2) {
return $regex;
}
+ /**
+ * Adjusts the heading levels of h1-h6 elements in a string in order to
+ * fit the needs of a particular location in the output HTML.
+ *
+ * For example, setting $modifier to 1 will change h1 into h2, h5 into h6,
+ * etc.
+ *
+ * If the adjusted tag for the heading would be invalid (e.g. h7 or h0),
+ * then the tag will be changed to a simple div.
+ *
+ * Any attributes of the adjusted elements will be preserved unchanged.
+ * For example, `` might become ``.
+ *
+ * As a general rule, this method should be called from theme templates
+ * rather than source files, since only the template really knows what level
+ * of adjustment is necessary.
+ *
+ * @param mixed $str The string in which to adjust heading levels.
+ * If a non-string value is given, it will be returned unchanged.
+ * @param ?int $modifier The amount by which to adjust heading levels.
+ * If null, all headings will be converted to div elements. Default: 0.
+ * @return mixed The adjusted version of $str.
+ */
+ public static function adjustHeadingLevels(mixed $str, ?int $modifier = 0): mixed
+ {
+ if (!is_string($str)) {
+ return $str;
+ }
+
+ return preg_replace_callback(
+ '/<(\/?)h(\d)([^>]*)>/u',
+ function ($matches) use ($modifier) {
+ $l = (int) $matches[2] + (int) $modifier;
+
+ if (!is_null($modifier) && $l >= 1 && $l <= 6) {
+ return '<' . $matches[1] . 'h' . $l . $matches[3] . '>';
+ }
+
+ return '<' . $matches[1] . 'div' . $matches[3] . '>';
+ },
+ $str,
+ );
+ }
+
/**
* Clean up the XML to make sure it doesn't contain invalid characters.
*
@@ -2297,6 +2353,9 @@ public static function obExit(?bool $header = null, ?bool $do_footer = null, boo
// Start up the session URL fixer.
ob_start('SMF\\QueryString::ob_sessrewrite');
+ // Force the browser not to collapse tabs inside posts, etc.
+ ob_start(fn ($buffer) => strtr($buffer, [self::TAB_SUBSTITUTE => '' . "\t" . ' ']));
+
if (!empty(Theme::$current->settings['output_buffers']) && is_string(Theme::$current->settings['output_buffers'])) {
$buffers = explode(',', Theme::$current->settings['output_buffers']);
} elseif (!empty(Theme::$current->settings['output_buffers'])) {
diff --git a/Sources/Verifier.php b/Sources/Verifier.php
index e926cfee56..87c2c65c8a 100644
--- a/Sources/Verifier.php
+++ b/Sources/Verifier.php
@@ -626,7 +626,7 @@ protected function setQuestions(): void
$this->questions[] = [
'id' => $q,
- 'q' => BBCodeParser::load()->parse($row['question']),
+ 'q' => Utils::adjustHeadingLevels(Parser::transform($row['question']), null),
'is_error' => !empty($incorrectQuestions) && in_array($q, $incorrectQuestions),
// Remember a previous submission?
'a' => isset($_REQUEST[$this->id . '_vv'], $_REQUEST[$this->id . '_vv']['q'], $_REQUEST[$this->id . '_vv']['q'][$q]) ? Utils::htmlspecialchars($_REQUEST[$this->id . '_vv']['q'][$q]) : '',
diff --git a/Themes/default/Agreement.template.php b/Themes/default/Agreement.template.php
index 36883412df..1fadc0f24d 100644
--- a/Themes/default/Agreement.template.php
+++ b/Themes/default/Agreement.template.php
@@ -47,7 +47,7 @@ function template_main()
echo '
- ', Utils::$context['agreement'], '
+ ', Utils::adjustHeadingLevels(Utils::$context['agreement'], 3), '
';
}
@@ -75,7 +75,7 @@ function template_main()
echo '
- ', Utils::$context['privacy_policy'], '
+ ', Utils::adjustHeadingLevels(Utils::$context['privacy_policy'], 3), '
';
}
diff --git a/Themes/default/Display.template.php b/Themes/default/Display.template.php
index 7a8805cc2c..9b138d2360 100644
--- a/Themes/default/Display.template.php
+++ b/Themes/default/Display.template.php
@@ -687,7 +687,7 @@ function template_single_post($message)
';
echo '
- ', $message['body'], '
+ ', Utils::adjustHeadingLevels($message['body'], 4), '
';
diff --git a/Themes/default/GenericList.template.php b/Themes/default/GenericList.template.php
index a3d2afed40..56027554d4 100644
--- a/Themes/default/GenericList.template.php
+++ b/Themes/default/GenericList.template.php
@@ -114,7 +114,7 @@ function template_show_list($list_id = null)
foreach ($row['data'] as $row_id => $row_data)
echo '
- ', $row_data['value'], '
+ ', Utils::adjustHeadingLevels($row_data['value'], 3), '
';
echo '
diff --git a/Themes/default/ModerationCenter.template.php b/Themes/default/ModerationCenter.template.php
index 1ddc564604..b3fb6ac7b5 100644
--- a/Themes/default/ModerationCenter.template.php
+++ b/Themes/default/ModerationCenter.template.php
@@ -329,7 +329,7 @@ function template_notes()
foreach (Utils::$context['notes'] as $note)
echo '
- ', ($note['can_delete'] ? ' ' : ''), $note['time'], ' ', $note['author']['link'], ': ', $note['text'], '
+ ', ($note['can_delete'] ? ' ' : ''), $note['time'], ' ', $note['author']['link'], ': ', Utils::adjustHeadingLevels($note['text'], 4), '
';
echo '
@@ -424,7 +424,7 @@ function template_unapproved_posts()
', str_replace(' ', ' ', Lang::getTxt('last_post_topic', ['post_link' => $item['time'], 'member_link' => '' . $item['poster']['link'] . ' '])), '
-
', $item['body'], '
+
', Utils::adjustHeadingLevels($item['body'], 5), '
', template_quickbuttons($quickbuttons, 'unapproved_posts'), '
';
@@ -546,7 +546,7 @@ function template_show_notice()
', Lang::$txt['show_notice_text'], '
- ', Utils::$context['notice_body'], '
+ ', Utils::adjustHeadingLevels(Utils::$context['notice_body'], 3), '
diff --git a/Themes/default/Packages.template.php b/Themes/default/Packages.template.php
index 93536cb5ae..fe87a1fc1a 100644
--- a/Themes/default/Packages.template.php
+++ b/Themes/default/Packages.template.php
@@ -93,7 +93,7 @@ function template_view_package()
', Lang::$txt['package_' . (Utils::$context['uninstalling'] ? 'un' : '') . 'install_readme'], '
- ', Utils::$context['package_readme'], '
+ ', Utils::adjustHeadingLevels(Utils::$context['package_readme'], 3), '
', Lang::$txt['package_available_readme_language'], '
';
@@ -116,7 +116,7 @@ function template_view_package()
', Lang::$txt['package_install_license'], '
- ', Utils::$context['package_license'], '
+ ', Utils::adjustHeadingLevels(Utils::$context['package_license'], 3), '
', Lang::$txt['package_available_license_language'], '
';
diff --git a/Themes/default/PersonalMessage.template.php b/Themes/default/PersonalMessage.template.php
index 8fc826829e..6a8d289890 100644
--- a/Themes/default/PersonalMessage.template.php
+++ b/Themes/default/PersonalMessage.template.php
@@ -977,7 +977,7 @@ function template_send()
- ', empty(Utils::$context['preview_message']) ? ' ' : Utils::$context['preview_message'], '
+ ', empty(Utils::$context['preview_message']) ? ' ' : Utils::adjustHeadingLevels(Utils::$context['preview_message'], 3), '
@@ -1249,7 +1249,7 @@ function onDocSent(XMLDoc)
', Lang::getTxt('pm_from', ['member' => Utils::$context['quoted_message']['member']['link']]), '
- ', Utils::$context['quoted_message']['body'], '
+ ', Utils::adjustHeadingLevels(Utils::$context['quoted_message']['body'], 3), '
';
@@ -1917,9 +1917,9 @@ function template_showPMDrafts()
#', $draft['counter'], '
-
+
', $draft['subject'], '
-
+
', Lang::getTxt('pm_to', ['list' => implode(Lang::$txt['sentence_list_separator'] . ' ', $draft['recipients']['to'])]), '
';
@@ -1934,7 +1934,7 @@ function template_showPMDrafts()
- ', $draft['body'], '
+ ', Utils::adjustHeadingLevels($draft['body'], 4), '
';
// Draft buttons
diff --git a/Themes/default/Post.template.php b/Themes/default/Post.template.php
index 3af1eba871..e26ec1c1db 100644
--- a/Themes/default/Post.template.php
+++ b/Themes/default/Post.template.php
@@ -83,7 +83,7 @@ function addPollOption()
- ', empty(Utils::$context['preview_message']) ? ' ' : Utils::$context['preview_message'], '
+ ', empty(Utils::$context['preview_message']) ? ' ' : Utils::adjustHeadingLevels(Utils::$context['preview_message'], 4), '
';
@@ -603,7 +603,7 @@ function addPollOption()
';
echo '
- ', $post['message'], '
';
+ ', Utils::adjustHeadingLevels($post['message'], 5), '
';
if (Utils::$context['can_quote'])
echo '
diff --git a/Themes/default/Printpage.template.php b/Themes/default/Printpage.template.php
index da26b43fd5..8ba03f3394 100644
--- a/Themes/default/Printpage.template.php
+++ b/Themes/default/Printpage.template.php
@@ -182,7 +182,7 @@ function template_main()
', Lang::getTxt('posted_by_member_time', $post), '
- ', $post['body'];
+ ', Utils::adjustHeadingLevels($post['body'], 2);
// Show attachment images
if (isset($_GET['images']) && !empty(Utils::$context['printattach'][$post['id_msg']]))
diff --git a/Themes/default/Profile.template.php b/Themes/default/Profile.template.php
index b50480430b..5f9577d794 100644
--- a/Themes/default/Profile.template.php
+++ b/Themes/default/Profile.template.php
@@ -355,7 +355,7 @@ function template_summary()
echo '
', Lang::$txt['profile_warning_level'], '
- ', Lang::formatTxt('{0, number, :: percent}', [Utils::$context['member']['warning']]), ' ';
+ ', Lang::formatText('{0, number, :: percent}', [Utils::$context['member']['warning']]), ' ';
// Can we provide information on what this means?
if (!empty(Utils::$context['warning_status']))
@@ -503,9 +503,9 @@ function template_showPosts()
#', $post['counter'], '
';
@@ -518,7 +518,7 @@ function template_showPosts()
echo '
- ', $post['body'], '
+ ', Utils::adjustHeadingLevels($post['body'], 4), '
';
@@ -668,7 +668,7 @@ function template_showDrafts()
#', $draft['counter'], '
-
+
', $draft['board']['name'], ' / ', $draft['topic']['link'], ' ';
if (!empty($draft['sticky']))
@@ -680,11 +680,11 @@ function template_showDrafts()
';
echo '
-
+
', Lang::getTxt('draft_saved_on', ['date' => $draft['time']]), '
- ', $draft['body'], '
+ ', Utils::adjustHeadingLevels($draft['body'], 4), '
';
@@ -2425,7 +2425,7 @@ function updateSlider(slideAmount)
', Lang::$txt['preview'], '
- ', !empty(Utils::$context['warning_data']['body_preview']) ? Utils::$context['warning_data']['body_preview'] : '', '
+ ', !empty(Utils::$context['warning_data']['body_preview']) ? Utils::adjustHeadingLevels(Utils::$context['warning_data']['body_preview'], 3) : '', '
diff --git a/Themes/default/Register.template.php b/Themes/default/Register.template.php
index 91d0d29929..62139380d8 100644
--- a/Themes/default/Register.template.php
+++ b/Themes/default/Register.template.php
@@ -30,7 +30,7 @@ function template_registration_agreement()
', Lang::$txt['registration_agreement'], '
-
', Utils::$context['agreement'], '
+
', Utils::adjustHeadingLevels(Utils::$context['agreement'], 3), '
';
if (!empty(Utils::$context['privacy_policy']))
@@ -39,7 +39,7 @@ function template_registration_agreement()
', Lang::$txt['privacy_policy'], '
-
', Utils::$context['privacy_policy'], '
+
', Utils::adjustHeadingLevels(Utils::$context['privacy_policy'], 3), '
';
echo '
diff --git a/Themes/default/ReportedContent.template.php b/Themes/default/ReportedContent.template.php
index 593c967d4c..e369297cb8 100644
--- a/Themes/default/ReportedContent.template.php
+++ b/Themes/default/ReportedContent.template.php
@@ -67,7 +67,7 @@ function template_reported_posts()
', Lang::getTxt('mc_reportedp_reported_by', ['list' => Lang::sentenceList($comments)]), '
- ', $report['body'], '
+ ', Utils::adjustHeadingLevels($report['body'], 5), '
';
// Reported post options
@@ -81,7 +81,7 @@ function template_reported_posts()
if (empty(Utils::$context['reports']))
echo '
-
', Lang::$txt['mc_reportedp_none_found'], '
+
', Lang::$txt['mc_reportedp_none_found'], '
';
echo '
@@ -216,7 +216,7 @@ function template_viewmodreport()
- ', Utils::$context['report']['body'], '
+ ', Utils::adjustHeadingLevels(Utils::$context['report']['body'], 3), '
@@ -226,7 +226,7 @@ function template_viewmodreport()
foreach (Utils::$context['report']['comments'] as $comment)
echo '
-
+
', Lang::getTxt(
'mc_modreport_whoreported_data',
[
@@ -234,8 +234,8 @@ function template_viewmodreport()
'datetime' => $comment['time'],
],
), '
-
-
', $comment['message'], '
+
+
', Utils::adjustHeadingLevels($comment['message'], null), '
';
echo '
@@ -248,7 +248,7 @@ function template_viewmodreport()
if (empty(Utils::$context['report']['mod_comments']))
echo '
';
foreach (Utils::$context['report']['mod_comments'] as $comment)
@@ -262,7 +262,7 @@ function template_viewmodreport()
echo '
-
', $comment['message'], '
+
', Utils::adjustHeadingLevels($comment['message'], null), '
';
}
@@ -442,7 +442,7 @@ function template_reported_members()
if (empty(Utils::$context['reports']))
echo '
-
', Lang::$txt['mc_reportedp_none_found'], '
+
', Lang::$txt['mc_reportedp_none_found'], '
';
echo '
@@ -511,7 +511,7 @@ function template_viewmemberreport()
foreach (Utils::$context['report']['comments'] as $comment)
echo '
-
+
', Lang::getTxt(
'mc_modreport_whoreported_data',
[
@@ -519,8 +519,8 @@ function template_viewmemberreport()
'datetime' => $comment['time'],
],
), '
-
-
', $comment['message'], '
+
+
', Utils::adjustHeadingLevels($comment['message'], null), '
';
echo '
@@ -533,7 +533,7 @@ function template_viewmemberreport()
if (empty(Utils::$context['report']['mod_comments']))
echo '
';
foreach (Utils::$context['report']['mod_comments'] as $comment)
@@ -545,7 +545,7 @@ function template_viewmemberreport()
echo '
-
', $comment['message'], '
+
', Utils::adjustHeadingLevels($comment['message'], null), '
';
}
diff --git a/Themes/default/SplitTopics.template.php b/Themes/default/SplitTopics.template.php
index e356b42166..f1f24c34c2 100644
--- a/Themes/default/SplitTopics.template.php
+++ b/Themes/default/SplitTopics.template.php
@@ -108,7 +108,7 @@ function template_select()
', Lang::getTxt('post_by_member', $message), '
', $message['time'], '
- ', $message['body'], '
+ ', Utils::adjustHeadingLevels($message['body'], 3), '
';
echo '
@@ -137,7 +137,7 @@ function template_select()
', Lang::getTxt('post_by_member', $message), '
', $message['time'], '
- ', $message['body'], '
+ ', Utils::adjustHeadingLevels($message['body'], 3), '
';
echo '
diff --git a/Themes/default/css/index.css b/Themes/default/css/index.css
index 78c8852ab0..a24e700fdc 100644
--- a/Themes/default/css/index.css
+++ b/Themes/default/css/index.css
@@ -11,6 +11,7 @@ body {
display: flex;
flex-direction: column;
min-height: 100vh;
+ tab-size: 4ch;
}
::selection {
text-shadow: none;
@@ -93,7 +94,7 @@ textarea {
}
/* Use a consistent monospace font everywhere */
-.monospace, .bbc_code, .phpcode, pre {
+.monospace, .bbc_code, .bbc_tt, .phpcode, pre {
font-family: "DejaVu Sans Mono", Menlo, Monaco, Consolas, monospace;
}
@@ -395,6 +396,11 @@ blockquote cite::before {
.expand_code {
max-height: none;
}
+/* Inline code */
+.bbc_tt {
+ background-color: rgba(127, 127, 127, 0.15);
+ padding: 0 0.2ch;
+}
/* Styling for BBC tags */
.bbc_link {
border-bottom: 1px solid #a8b6cf;
@@ -412,11 +418,15 @@ blockquote cite::before {
.bbc_table {
font: inherit;
color: inherit;
+ border: 1px solid #ddd;
+ border-collapse: collapse;
}
+.bbc_table th,
.bbc_table td {
- font: inherit;
color: inherit;
vertical-align: top;
+ padding: 4px 8px;
+ border: 1px solid #ddd;
}
.bbc_list {
text-align: left;
@@ -448,6 +458,30 @@ blockquote cite::before {
max-height: none;
max-width: 100%;
}
+.bbc_h1, .bbc_h2, .bbc_h3, .bbc_h4, .bbc_h5, .bbc_h6 {
+ font-weight: bold;
+ margin: 0.5em 0;
+ line-height: normal;
+}
+.bbc_h1 {
+ font-size: 2em;
+}
+.bbc_h2 {
+ font-size: 1.8em;
+}
+.bbc_h3 {
+ font-size: 1.6em;
+}
+.bbc_h4 {
+ font-size: 1.4em;
+}
+.bbc_h5 {
+ font-size: 1.2em;
+}
+.bbc_h6 {
+ font-size: 1em;
+}
+
/* No image should have a border when linked. */
a img {
border: 0;
@@ -1468,7 +1502,7 @@ ul li.greeting {
margin: 1em 0 2em;
}
#post_event .roundframe {
- padding: 12px 12%;
+ padding: 12px 8%;
overflow: auto;
}
#post_event fieldset {
@@ -3535,7 +3569,7 @@ img.smiley {
}
#quickreply_options .roundframe {
margin: 0;
- padding: 8px 10% 12px 10%;
+ padding: 8px 8% 12px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
@@ -3545,8 +3579,7 @@ img.smiley {
}
/* Styles for edit post section */
form#postmodify .roundframe {
- padding: 12px 12%;
- margin: 12px 0 0 0;
+ padding: 12px 8%;
}
#post_header {
padding: 6px;
diff --git a/Themes/default/css/jquery.sceditor.default.css b/Themes/default/css/jquery.sceditor.default.css
index e1f647b8eb..57a58d917f 100644
--- a/Themes/default/css/jquery.sceditor.default.css
+++ b/Themes/default/css/jquery.sceditor.default.css
@@ -7,6 +7,7 @@ html, p, code::before, table {
color: #111;
line-height: 1.25;
overflow: visible;
+ tab-size: 4ch;
}
html {
height: 100%;
@@ -69,6 +70,12 @@ code::before, code {
text-align: left;
}
+span.phpcode, font[face=monospace] {
+ background-color: rgba(127, 127, 127, 0.25);
+ padding: 0 0.2ch;
+ display: inline;
+}
+
blockquote {
margin: 0 0 8px 0;
padding: 6px 10px;
@@ -99,8 +106,27 @@ blockquote br:last-child {
}
h1, h2, h3, h4, h5, h6 {
- padding: 0;
- margin: 0;
+ font-weight: bold;
+ margin: 0.5em 0;
+ line-height: normal;
+}
+h1 {
+ font-size: 2rem;
+}
+h2 {
+ font-size: 1.8rem;
+}
+h3 {
+ font-size: 1.6rem;
+}
+h4 {
+ font-size: 1.4rem;
+}
+h5 {
+ font-size: 1.2rem;
+}
+h6 {
+ font-size: 1rem;
}
/* Make sure images stay within bounds */
diff --git a/Themes/default/css/markdown.css b/Themes/default/css/markdown.css
new file mode 100644
index 0000000000..b3dfeda9f5
--- /dev/null
+++ b/Themes/default/css/markdown.css
@@ -0,0 +1,105 @@
+.inner p,
+.inner blockquote,
+.inner ul,
+.inner ol,
+.inner li,
+.inner table,
+.list_posts p,
+.list_posts blockquote,
+.list_posts ul,
+.list_posts ol,
+.list_posts li,
+.list_posts table,
+#preview_body p,
+#preview_body blockquote,
+#preview_body ul,
+#preview_body ol,
+#preview_body li,
+#preview_body table {
+ margin-top: 1.1em;
+ margin-bottom: 1.1em;
+}
+
+/* No margin-top on the first block element */
+.inner p:first-child,
+.inner ul:first-child,
+.inner ol:first-child,
+.inner li:first-child,
+.inner table:first-child,
+.list_posts p:first-child,
+.list_posts ul:first-child,
+.list_posts ol:first-child,
+.list_posts li:first-child,
+.list_posts table:first-child,
+#preview_body p:first-child,
+#preview_body ul:first-child,
+#preview_body ol:first-child,
+#preview_body li:first-child,
+#preview_body table:first-child,
+blockquote cite:first-child + p,
+blockquote cite:first-child + ul,
+blockquote cite:first-child + ol,
+blockquote cite:first-child + ul > li:first-child,
+blockquote cite:first-child + ol > li:first-child,
+blockquote cite:first-child + table {
+ margin-top: 0;
+}
+
+/* No margin-bottom on the last block element */
+.inner p:last-child,
+.inner ul:last-child,
+.inner ol:last-child,
+.inner li:last-child,
+.inner table:last-child,
+.list_posts p:last-child,
+.list_posts ul:last-child,
+.list_posts ol:last-child,
+.list_posts li:last-child,
+.list_posts table:last-child,
+#preview_body p:last-child,
+#preview_body ul:last-child,
+#preview_body ol:last-child,
+#preview_body li:last-child,
+#preview_body table:last-child,
+blockquote p:last-child,
+blockquote ul:last-child,
+blockquote ol:last-child,
+blockquote ul:last-child > li:last-child,
+blockquote ol:last-child > li:last-child,
+blockquote table:last-child {
+ margin-bottom: 0;
+}
+
+/* No margin-top on blocks immediately following a float */
+.bbc_float + p,
+.bbc_float + * > p:first-child,
+.bbc_float + blockquote,
+.bbc_float + ul,
+.bbc_float + ol,
+.bbc_float + ul > li:first-child,
+.bbc_float + ol > li:first-child,
+.bbc_float + table {
+ margin-top: 0;
+}
+
+/* No margin-top on blocks immediately following a blank line created by hard line breaks. */
+.inner br + br + p,
+.inner br + br + ul,
+.inner br + br + ol,
+.inner br + br + ul > li:first-child,
+.inner br + br + ol > li:first-child,
+.inner br + br + table,
+.list_posts br + br + p,
+.list_posts br + br + ul,
+.list_posts br + br + ol,
+.list_posts br + br + ul > li:first-child,
+.list_posts br + br + ol > li:first-child,
+.list_posts br + br + table,
+#preview_body br + br + p,
+#preview_body br + br + ul,
+#preview_body br + br + ol,
+#preview_body br + br + ul > li:first-child,
+#preview_body br + br + ol > li:first-child,
+#preview_body br + br + table {
+ margin-top: 0;
+}
\ No newline at end of file
diff --git a/Themes/default/images/bbc/heading.png b/Themes/default/images/bbc/heading.png
new file mode 100644
index 0000000000..6bcc65efb5
Binary files /dev/null and b/Themes/default/images/bbc/heading.png differ
diff --git a/Themes/default/images/bbc/tt.png b/Themes/default/images/bbc/tt.png
new file mode 100644
index 0000000000..92a8ad23e0
Binary files /dev/null and b/Themes/default/images/bbc/tt.png differ
diff --git a/Themes/default/scripts/jquery.sceditor.smf.js b/Themes/default/scripts/jquery.sceditor.smf.js
index 7f7d6d5412..1e897a2fd9 100644
--- a/Themes/default/scripts/jquery.sceditor.smf.js
+++ b/Themes/default/scripts/jquery.sceditor.smf.js
@@ -576,13 +576,21 @@
var isPatched = false;
sceditor.create = function (textarea, options) {
+ textarea.value = textarea.value.replaceAll(/\t/, '[tab]');
+
// Call the original create function
createFn(textarea, options);
+ textarea.value = textarea.value.replaceAll(/\[tab\]/, '\t');
+
// Constructor isn't exposed so get reference to it when
// creating the first instance and extend it then
var instance = sceditor.instance(textarea);
if (!isPatched && instance) {
+ const wysiwygEditor = instance.getContentAreaContainer();
+ const editorContainer = wysiwygEditor.parentElement;
+ const sourceEditor = editorContainer.querySelector("textarea");
+
sceditor.utils.extend(instance.constructor.prototype, extensionMethods);
window.addEventListener('beforeunload', instance.updateOriginal, false);
@@ -591,9 +599,26 @@
* toolbars and tons of smilies play havoc with this.
* Only resize the text areas instead.
*/
- document.querySelector(".sceditor-container").removeAttribute("style");
- document.querySelector(".sceditor-container textarea").style.height = options.height;
- document.querySelector(".sceditor-container textarea").style.flexBasis = options.height;
+ editorContainer.removeAttribute("style");
+ sourceEditor.style.height = options.height;
+ sourceEditor.style.flexBasis = options.height;
+
+ // Override these functions in order to convince SCEditor not to
+ // delete tabs. Supporting Markdown means we need to keep them.
+ const getSourceVal = instance.getSourceEditorValue;
+ const setSourceVal = instance.setSourceEditorValue;
+
+ instance.getSourceEditorValue = function (filter) {
+ if (filter !== false) {
+ sourceEditor.value = sourceEditor.value.replaceAll(/\t/, '[tab]');
+ }
+
+ return getSourceVal(filter);
+ };
+
+ instance.setSourceEditorValue = function (value) {
+ setSourceVal(value.replaceAll(/\[tab\]/, '\t'));
+ };
isPatched = true;
}
@@ -851,6 +876,83 @@ sceditor.command.set(
}
);
+sceditor.command.set(
+ 'heading', {
+ _dropDown: function (editor, caller, callback) {
+ var content = document.createElement('div');
+
+ for (var i = 1; i <= 6; i++) {
+ let opt = document.createElement('a');
+ opt.href = '#',
+ opt.dataset.tag = 'h' + i;
+ opt.innerText = 'H' + i;
+ opt.style.display = 'block';
+ opt.classList.add('bbc_h' + i);
+ content.appendChild(opt);
+ }
+
+ if (!editor.sourceMode()) {
+ let opt = document.createElement('a');
+ opt.href = '#',
+ opt.dataset.tag = '';
+ opt.innerText = "\u2014";
+ opt.style.display = 'block';
+ content.appendChild(opt);
+ }
+
+ for (const elem of content.querySelectorAll("a")) {
+ elem.addEventListener("click", function (e) {
+ callback(elem.dataset.tag);
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+ }
+
+ editor.createDropDown(caller, 'heading-picker', content);
+ },
+ state: function (parent, firstBlock) {
+ return sceditor.dom.closest(this.currentNode(), 'h1, h2, h3, h4, h5, h6') ? 1 : 0;
+ },
+ txtExec: function (caller) {
+ var editor = this;
+
+ editor.commands.heading._dropDown(editor, caller, function (tag) {
+ let caretPos = editor.sourceEditorCaret().start;
+
+ if (tag.match(/h[1-6]/)) {
+ editor.insert('[' + tag + ']', '[/' + tag + ']');
+ editor.toggleSourceMode();
+ editor.toggleSourceMode();
+ editor.sourceEditorCaret({start: caretPos, end: caretPos});
+ }
+ });
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ editor.commands.heading._dropDown(editor, caller, function (tag) {
+ const rangeHelper = editor.getRangeHelper()
+ const container = rangeHelper.parentNode().parentNode;
+ const containerParent = container.parentNode;
+ const content = container.innerHTML;
+
+ if (
+ container.nodeType === Node.ELEMENT_NODE
+ && container.nodeName.match(/H[1-6]/)
+ ) {
+ let newElement = document.createElement(tag.match(/h[1-6]/) ? tag : 'p');
+ newElement.innerHTML = content;
+ container.replaceWith(newElement);
+ containerParent.normalize();
+ rangeHelper.selectOuterText(0, content.length);
+ } else if (tag.match(/h[1-6]/)) {
+ editor.insert('[' + tag + ']', '[/' + tag + ']');
+ }
+ });
+ },
+ }
+);
+
sceditor.command.set(
'maximize', {
shortcut: ''
@@ -935,6 +1037,60 @@ sceditor.command.set(
}
);
+sceditor.command.set(
+ 'tt', {
+ state: function (parent, firstBlock) {
+ if (this.inSourceMode()) {
+ return 0;
+ }
+
+ let currNode = sceditor.dom.closest(this.currentNode(), 'font');
+
+ if (!currNode) {
+ return 0;
+ }
+
+ let font = currNode.getAttribute('face');
+
+ return (font === 'monospace') ? 1 : 0;
+ },
+ exec: function(caller) {
+ let currNode = sceditor.dom.closest(this.currentNode(), 'font');
+
+ if (!currNode) {
+ this.execCommand('fontname', 'monospace');
+ } else {
+ let font = currNode.getAttribute('face');
+
+ if (font === 'monospace') {
+ this.execCommand('removeFormat');
+ } else {
+ this.execCommand('fontname', 'monospace');
+ }
+ }
+ },
+ txtExec: function(caller) {
+ this.insert('[tt]', '[/tt]');
+ }
+ }
+);
+
+// This pseudo-BBCode exists solely to convince SCEditor not to delete tab characters.
+sceditor.formats.bbcode.set(
+ 'tab', {
+ tags: {
+ span: {
+ class: 'tab'
+ }
+ },
+ allowsEmpty: true,
+ isSelfClosing: true,
+ isInline: true,
+ format: '\t',
+ html: '\t '
+ }
+);
+
sceditor.formats.bbcode.set(
'abbr', {
tags: {
@@ -1400,9 +1556,26 @@ sceditor.formats.bbcode.set(
sceditor.formats.bbcode.set(
'php', {
- isInline: false,
- format: "[php]{0}[/php]",
- html: '{0}'
+ tags: {
+ span: {
+ 'class': 'phpcode'
+ }
+ },
+ isInline: true,
+ format: '[php]{0}[/php]',
+ html: '{0} '
+ }
+);
+
+sceditor.formats.bbcode.set(
+ 'tt', {
+ tags: {
+ font: {
+ 'face': 'monospace'
+ }
+ },
+ format: '[tt]{0}[/tt]',
+ html: '{0} '
}
);
@@ -1414,9 +1587,6 @@ sceditor.formats.bbcode.set(
isInline: false,
allowedChildren: ['#', '#newline'],
format: function (element, content) {
- if ($(element).hasClass('php'))
- return '[php]' + content.replace('[', '[') + '[/php]';
-
var
dom = sceditor.dom,
attr = dom.attr,
@@ -1509,6 +1679,11 @@ sceditor.formats.bbcode.set(
// Strip all quotes
font = font.replace(/['"]/g, '');
+ // To make [tt] work, we need to add an exception to the [font] BBC.
+ if (font === 'monospace') {
+ return content;
+ }
+
return '[font=' + font + ']' + content + '[/font]';
}
}
@@ -1585,4 +1760,71 @@ sceditor.formats.bbcode.set(
},
html: ''
}
+);
+
+sceditor.formats.bbcode.set(
+ 'h1', {
+ tags: {
+ h1: null,
+ },
+ isInline: false,
+ skipLastLineBreak: true,
+ format: '[h1]{0}[/h1]',
+ html: '{0} '
+ }
+);
+sceditor.formats.bbcode.set(
+ 'h2', {
+ tags: {
+ h2: null,
+ },
+ isInline: false,
+ skipLastLineBreak: true,
+ format: '[h2]{0}[/h2]',
+ html: '{0} '
+ }
+);
+sceditor.formats.bbcode.set(
+ 'h3', {
+ tags: {
+ h3: null,
+ },
+ isInline: false,
+ skipLastLineBreak: true,
+ format: '[h3]{0}[/h3]',
+ html: '{0} '
+ }
+);
+sceditor.formats.bbcode.set(
+ 'h4', {
+ tags: {
+ h4: null,
+ },
+ isInline: false,
+ skipLastLineBreak: true,
+ format: '[h4]{0}[/h4]',
+ html: '{0} '
+ }
+);
+sceditor.formats.bbcode.set(
+ 'h5', {
+ tags: {
+ h5: null,
+ },
+ isInline: false,
+ skipLastLineBreak: true,
+ format: '[h5]{0}[/h5]',
+ html: '{0} '
+ }
+);
+sceditor.formats.bbcode.set(
+ 'h6', {
+ tags: {
+ h6: null,
+ },
+ isInline: false,
+ skipLastLineBreak: true,
+ format: '[h6]{0}[/h6]',
+ html: '{0} '
+ }
);
\ No newline at end of file
diff --git a/other/upgrade_2-1_MySQL.sql b/other/upgrade_2-1_MySQL.sql
index da91c8dd63..f7779a37a2 100644
--- a/other/upgrade_2-1_MySQL.sql
+++ b/other/upgrade_2-1_MySQL.sql
@@ -201,8 +201,8 @@ if (version_compare(trim(strtolower(@Config::$modSettings['smfVersion'])), '2.1.
while ($row = Db::$db->fetch_assoc($request))
{
$inserts[] = array(
- 'name' => Utils::htmlspecialchars(strip_tags(SMF\BBCodeParser::load()->unparse($row['name']))),
- 'description' => Utils::htmlspecialchars(strip_tags(SMF\BBCodeParser::load()->unparse($row['description']))),
+ 'name' => Utils::htmlspecialchars(strip_tags(SMF\Parser::transform($row['name'], SMF\Parser::OUTPUT_BBC))),
+ 'description' => Utils::htmlspecialchars(strip_tags(SMF\Parser::transform($row['description'], SMF\Parser::OUTPUT_BBC))),
'id' => $row['id'],
);
}
diff --git a/other/upgrade_2-1_PostgreSQL.sql b/other/upgrade_2-1_PostgreSQL.sql
index 96e2fedced..0343dcdc2b 100644
--- a/other/upgrade_2-1_PostgreSQL.sql
+++ b/other/upgrade_2-1_PostgreSQL.sql
@@ -392,8 +392,8 @@ if (version_compare(trim(strtolower(@Config::$modSettings['smfVersion'])), '2.1.
while ($row = Db::$db->fetch_assoc($request))
{
$inserts[] = array(
- 'name' => Utils::htmlspecialchars(strip_tags(SMF\BBCodeParser::load()->unparse($row['name']))),
- 'description' => Utils::htmlspecialchars(strip_tags(SMF\BBCodeParser::load()->unparse($row['description']))),
+ 'name' => Utils::htmlspecialchars(strip_tags(SMF\Parser::transform($row['name'], SMF\Parser::OUTPUT_BBC)),
+ 'description' => Utils::htmlspecialchars(strip_tags(SMF\Parser::transform($row['description'], SMF\Parser::OUTPUT_BBC)),
'id' => $row['id'],
);
}