From 0fe1189c9ca0e525e783955193b5abd43e2072b3 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Sun, 23 Jun 2024 12:55:08 -0600 Subject: [PATCH 01/18] Respects enablePostHTML setting even when BBCode is disabled Signed-off-by: Jon Stovell --- Sources/BBCodeParser.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Sources/BBCodeParser.php b/Sources/BBCodeParser.php index 397f3df00f..089307f71a 100644 --- a/Sources/BBCodeParser.php +++ b/Sources/BBCodeParser.php @@ -996,6 +996,8 @@ public function parse(string $message, bool $smileys = true, string|int $cache_i $this->message = $this->parseSmileys($this->message); } + $this->message = $this->fixHtml($this->message); + return $this->message; } @@ -2951,7 +2953,21 @@ protected function fixHtml(string $data): string 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 $this->enable_bbc ? '[iurl="' . $matches[2] . '"]' . $matches[3] . '[/iurl]' : '' . $matches[3] . ''; + } + + return $this->enable_bbc ? '[url="' . $matches[2] . '"]' . $matches[3] . '[/url]' : '' . $matches[3] . ''; + }, + $data, + ); //
should be empty. $empty_tags = ['br', 'hr']; From f29f244cdcdd953acd28fc412355d7a67b5d3464 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Fri, 14 Jun 2024 01:43:50 -0600 Subject: [PATCH 02/18] Improves semantics of BBC output Signed-off-by: Jon Stovell --- Sources/BBCodeParser.php | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Sources/BBCodeParser.php b/Sources/BBCodeParser.php index 089307f71a..dbc0f870c3 100644 --- a/Sources/BBCodeParser.php +++ b/Sources/BBCodeParser.php @@ -416,8 +416,8 @@ class BBCodeParser ], [ 'tag' => 'b', - 'before' => '', - 'after' => '', + 'before' => '', + 'after' => '', ], // Legacy (equivalent to [ltr] or [rtl]) [ @@ -557,8 +557,8 @@ class BBCodeParser ], [ 'tag' => 'i', - 'before' => '', - 'after' => '', + 'before' => '', + 'after' => '', ], [ 'tag' => 'img', @@ -622,7 +622,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' => '
    ', 'after' => '
', @@ -630,6 +630,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 +685,7 @@ class BBCodeParser [ 'tag' => 'php', 'type' => 'unparsed_content', - 'content' => '$1', + 'content' => '$1', 'validate' => __CLASS__ . '::phpValidate', 'block_level' => false, 'disabled_content' => '$1', From 92a6ca0f998653cfb55d1ed6ca72a89b7b584c77 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Wed, 19 Jun 2024 13:36:04 -0600 Subject: [PATCH 03/18] Restores the tt BBCode to full status Also fixes WYSIWYG bugs with the php BBCode Signed-off-by: Jon Stovell --- Languages/en_US/Editor.php | 1 + Sources/BBCodeParser.php | 5 +- Sources/Editor.php | 5 ++ Sources/Utils.php | 2 +- Themes/default/css/index.css | 7 +- .../default/css/jquery.sceditor.default.css | 6 ++ Themes/default/images/bbc/tt.png | Bin 0 -> 912 bytes Themes/default/scripts/jquery.sceditor.smf.js | 69 ++++++++++++++++-- 8 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 Themes/default/images/bbc/tt.png diff --git a/Languages/en_US/Editor.php b/Languages/en_US/Editor.php index 05b0c64641..2b48061323 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):'; diff --git a/Sources/BBCodeParser.php b/Sources/BBCodeParser.php index dbc0f870c3..7ff97d1b8e 100644 --- a/Sources/BBCodeParser.php +++ b/Sources/BBCodeParser.php @@ -837,11 +837,10 @@ class BBCodeParser 'disabled_before' => '', 'disabled_after' => '', ], - // Legacy (the element is dead) [ 'tag' => 'tt', - 'before' => '', - 'after' => '', + 'before' => '', + 'after' => '', ], [ 'tag' => 'u', diff --git a/Sources/Editor.php b/Sources/Editor.php index 13aa836183..e1ad7644fc 100644 --- a/Sources/Editor.php +++ b/Sources/Editor.php @@ -588,6 +588,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'], diff --git a/Sources/Utils.php b/Sources/Utils.php index e30c4090f6..9c3d6ca905 100644 --- a/Sources/Utils.php +++ b/Sources/Utils.php @@ -271,7 +271,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' => [ diff --git a/Themes/default/css/index.css b/Themes/default/css/index.css index 78c8852ab0..210cbbfc31 100644 --- a/Themes/default/css/index.css +++ b/Themes/default/css/index.css @@ -93,7 +93,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 +395,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; diff --git a/Themes/default/css/jquery.sceditor.default.css b/Themes/default/css/jquery.sceditor.default.css index e1f647b8eb..13d635180d 100644 --- a/Themes/default/css/jquery.sceditor.default.css +++ b/Themes/default/css/jquery.sceditor.default.css @@ -69,6 +69,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; diff --git a/Themes/default/images/bbc/tt.png b/Themes/default/images/bbc/tt.png new file mode 100644 index 0000000000000000000000000000000000000000..92a8ad23e028c7788dd8de0e0e3806441cd53fff GIT binary patch literal 912 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBC=rkv;hE;^%b*2hb1*P5 z3NbJPS&Tr)(4NV_0%kKX08Ih{<^_xh*#%5+S%C%22sTKeT1WgF1_maB%#etZ2wxwo zh+i#(Mch>H3D2 zmX`VkM*2oZx=P7{9O-#x!EwNQn0$BtH z5O=0lWFlI_Kx)7X=q2Ca2mNLbV_Xp{oX46M;+AMj1{NJz`r z`)|(5*3TuZOFl;57jpVMvu)>2-^qf_&z2axy3xe=sqXcqhUQyWKPW6zF!=wrv}XGA x|49uBM}B#je6}fRW@OOatM986_wnpG^Jkl8`mH|Oe;t%yJzf1=);T3K0RVQp7pDLK literal 0 HcmV?d00001 diff --git a/Themes/default/scripts/jquery.sceditor.smf.js b/Themes/default/scripts/jquery.sceditor.smf.js index 7f7d6d5412..79910a52e0 100644 --- a/Themes/default/scripts/jquery.sceditor.smf.js +++ b/Themes/default/scripts/jquery.sceditor.smf.js @@ -935,6 +935,44 @@ 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]'); + } + } +); + sceditor.formats.bbcode.set( 'abbr', { tags: { @@ -1400,9 +1438,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 +1469,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 +1561,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]'; } } From 816826dce6da763cec0fee9b27a64ab719f92d71 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Tue, 11 Jun 2024 00:03:48 -0600 Subject: [PATCH 04/18] Marks first row of BBC tables as table header Signed-off-by: Jon Stovell --- Sources/BBCodeParser.php | 8 ++++++++ Themes/default/css/index.css | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Sources/BBCodeParser.php b/Sources/BBCodeParser.php index 7ff97d1b8e..c30eeb1b6f 100644 --- a/Sources/BBCodeParser.php +++ b/Sources/BBCodeParser.php @@ -2702,6 +2702,14 @@ protected function parseMessage(): void $this->message = strtr($this->message, ["\n" => '']); } + // Transform the first table row into a table header and wrap the rest + // in table body tags. + $this->message = preg_replace_callback( + '/(\X*?)<\/tr>(\X*?)<\/table>/u', + fn ($matches) => '
' . preg_replace('~()~', '$1th$2', $matches[1]) . '' . $matches[2] . '
', + $this->message, + ); + if ($this->message !== '' && $this->message[0] === ' ') { $this->message = ' ' . substr($this->message, 1); } diff --git a/Themes/default/css/index.css b/Themes/default/css/index.css index 210cbbfc31..e6a3c6815e 100644 --- a/Themes/default/css/index.css +++ b/Themes/default/css/index.css @@ -417,11 +417,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; From 731e67a759fef03853b6e00a54c54bc5ac67525c Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Wed, 19 Jun 2024 16:51:56 -0600 Subject: [PATCH 05/18] Adds support for h1-h6 BBCode tags This is complicated, because we need to adjust the HTML tags in the output depending on the context in which the HTML is shown. Signed-off-by: Jon Stovell --- Languages/en_US/Editor.php | 1 + Sources/Actions/Admin/News.php | 4 +- Sources/Actions/Admin/Smileys.php | 4 +- Sources/Actions/JavaScriptModify.php | 2 + Sources/Actions/Profile/Tracking.php | 4 +- Sources/Actions/XmlHttp.php | 9 +- Sources/BBCodeParser.php | 41 ++++- Sources/Editor.php | 5 + Sources/PackageManager/PackageManager.php | 2 +- Sources/Profile.php | 4 +- Sources/ServerSideIncludes.php | 2 +- Sources/Theme.php | 2 +- Sources/User.php | 4 +- Sources/Utils.php | 44 ++++++ Sources/Verifier.php | 2 +- Themes/default/Agreement.template.php | 4 +- Themes/default/Display.template.php | 2 +- Themes/default/GenericList.template.php | 2 +- Themes/default/ModerationCenter.template.php | 6 +- Themes/default/Packages.template.php | 4 +- Themes/default/PersonalMessage.template.php | 10 +- Themes/default/Post.template.php | 4 +- Themes/default/Printpage.template.php | 2 +- Themes/default/Profile.template.php | 16 +- Themes/default/Register.template.php | 4 +- Themes/default/ReportedContent.template.php | 28 ++-- Themes/default/SplitTopics.template.php | 4 +- Themes/default/css/index.css | 31 +++- .../default/css/jquery.sceditor.default.css | 23 ++- Themes/default/images/bbc/heading.png | Bin 0 -> 203 bytes Themes/default/scripts/jquery.sceditor.smf.js | 144 ++++++++++++++++++ 31 files changed, 350 insertions(+), 64 deletions(-) create mode 100644 Themes/default/images/bbc/heading.png diff --git a/Languages/en_US/Editor.php b/Languages/en_US/Editor.php index 2b48061323..a1b67e8cbf 100644 --- a/Languages/en_US/Editor.php +++ b/Languages/en_US/Editor.php @@ -61,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/Sources/Actions/Admin/News.php b/Sources/Actions/Admin/News.php index 8d9f454534..6f4c5fe5c6 100644 --- a/Sources/Actions/Admin/News.php +++ b/Sources/Actions/Admin/News.php @@ -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;"> }' . @@ -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) . '
'; } /** diff --git a/Sources/Actions/Admin/Smileys.php b/Sources/Actions/Admin/Smileys.php index fb75deacfb..f65ccd2eb8 100644 --- a/Sources/Actions/Admin/Smileys.php +++ b/Sources/Actions/Admin/Smileys.php @@ -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/JavaScriptModify.php b/Sources/Actions/JavaScriptModify.php index 5642bbae25..86a2d4fa75 100644 --- a/Sources/Actions/JavaScriptModify.php +++ b/Sources/Actions/JavaScriptModify.php @@ -295,6 +295,8 @@ public function execute(): void 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'] = Utils::adjustHeadingLevels(Utils::$context['message']['body'], null); } // Topic? elseif (empty($post_errors)) { diff --git a/Sources/Actions/Profile/Tracking.php b/Sources/Actions/Profile/Tracking.php index f043699e8a..ae0854d4df 100644 --- a/Sources/Actions/Profile/Tracking.php +++ b/Sources/Actions/Profile/Tracking.php @@ -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(BBCodeParser::load()->parse($extra['previous']), null) : $extra['previous']) : '', + 'after' => !empty($extra['new']) ? ($parse_bbc ? Utils::adjustHeadingLevels(BBCodeParser::load()->parse($extra['new']), null) : $extra['new']) : '', 'time' => Time::create('@' . $row['log_time'])->format(), ]; } diff --git a/Sources/Actions/XmlHttp.php b/Sources/Actions/XmlHttp.php index f18354edb2..602072c565 100644 --- a/Sources/Actions/XmlHttp.php +++ b/Sources/Actions/XmlHttp.php @@ -167,7 +167,7 @@ public function newspreview(): void 'identifier' => 'parsedNews', 'children' => [ [ - 'value' => BBCodeParser::load()->parse($news), + 'value' => Utils::adjustHeadingLevels(BBCodeParser::load()->parse($news), null), ], ], ], @@ -241,6 +241,9 @@ public function sig_preview(): void $current_signature = !empty($current_signature) ? BBCodeParser::load()->parse($current_signature, true, 'sig' . $user, $allowedTags) : Lang::$txt['no_signature_set']; + + $current_signature = Utils::adjustHeadingLevels($current_signature, null); + $preview_signature = !empty($_POST['signature']) ? Utils::htmlspecialchars($_POST['signature']) : Lang::$txt['no_signature_preview']; $validation = Profile::validateSignature($preview_signature); @@ -252,6 +255,8 @@ public function sig_preview(): void Lang::censorText($preview_signature); $preview_signature = BBCodeParser::load()->parse($preview_signature, true, 'sig' . $user, $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']]; @@ -355,6 +360,8 @@ public function warning_preview(): void Msg::preparsecode($warning_body, false, !empty(Config::$modSettings['autoLinkUrls'])); $warning_body = BBCodeParser::load()->parse($warning_body); + + $warning_body = Utils::adjustHeadingLevels($warning_body, null); } Utils::$context['preview_message'] = $warning_body; diff --git a/Sources/BBCodeParser.php b/Sources/BBCodeParser.php index c30eeb1b6f..7223a80e3c 100644 --- a/Sources/BBCodeParser.php +++ b/Sources/BBCodeParser.php @@ -542,6 +542,45 @@ 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', @@ -3506,7 +3545,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]); diff --git a/Sources/Editor.php b/Sources/Editor.php index e1ad7644fc..3e4174c342 100644 --- a/Sources/Editor.php +++ b/Sources/Editor.php @@ -610,6 +610,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/PackageManager/PackageManager.php b/Sources/PackageManager/PackageManager.php index e97ec992f2..5c14b03c36 100644 --- a/Sources/PackageManager/PackageManager.php +++ b/Sources/PackageManager/PackageManager.php @@ -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(BBCodeParser::load()->parse(preg_replace('~\[[/]?html\]~i', '', Utils::htmlspecialchars($package['description']))), null); } $package['is_installed'] = isset($installed_mods[$package['id']]); diff --git a/Sources/Profile.php b/Sources/Profile.php index 1d1b85ed1a..8f0a581593 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(BBCodeParser::load()->parse($output_html), null); } elseif ($cf_def['field_type'] == 'textarea') { // Allow for newlines at least $output_html = strtr($output_html, ["\n" => '
']); @@ -1350,7 +1350,7 @@ 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'] = Utils::adjustHeadingLevels(BBCodeParser::load()->parse($signature, true, 'sig' . $this->id, BBCodeParser::getSigTags()), null); Utils::$context['member']['signature'] = $_POST['signature']; } diff --git a/Sources/ServerSideIncludes.php b/Sources/ServerSideIncludes.php index 2f47b48e39..b46296d0dc 100644 --- a/Sources/ServerSideIncludes.php +++ b/Sources/ServerSideIncludes.php @@ -2308,7 +2308,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/Theme.php b/Sources/Theme.php index 4e38a1ea13..a3acf46dba 100644 --- a/Sources/Theme.php +++ b/Sources/Theme.php @@ -774,7 +774,7 @@ 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] = Utils::adjustHeadingLevels(BBCodeParser::load()->parse(stripslashes(trim(Utils::$context['news_lines'][$i])), true, 'news' . $i), null); } if (!empty(Utils::$context['news_lines']) && (!empty(Config::$modSettings['allow_guestAccess']) || User::$me->is_logged)) { diff --git a/Sources/User.php b/Sources/User.php index b5652afc60..bafea47b68 100644 --- a/Sources/User.php +++ b/Sources/User.php @@ -1219,6 +1219,8 @@ public function format(bool $display_custom_fields = false): array 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'] = Utils::adjustHeadingLevels($this->formatted['signature'], null); } // Are we also loading the member's custom fields? @@ -1252,7 +1254,7 @@ public function format(bool $display_custom_fields = false): array // BBC? if ($custom['bbc']) { - $value = BBCodeParser::load()->parse($value); + $value = Utils::adjustHeadingLevels(BBCodeParser::load()->parse($value), null); } // ... or checkbox? elseif (isset($custom['type']) && $custom['type'] == 'check') { diff --git a/Sources/Utils.php b/Sources/Utils.php index 9c3d6ca905..6cd0452dbf 100644 --- a/Sources/Utils.php +++ b/Sources/Utils.php @@ -1177,6 +1177,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. * diff --git a/Sources/Verifier.php b/Sources/Verifier.php index e926cfee56..7ca26947c1 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(BBCodeParser::load()->parse($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 ' '; 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'], ' '; 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 '
    -

    ', Lang::$txt['mc_modreport_no_mod_comment'], '

    +
    ', Lang::$txt['mc_modreport_no_mod_comment'], '
    '; 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 '
    -

    ', Lang::$txt['mc_modreport_no_mod_comment'], '

    +
    ', Lang::$txt['mc_modreport_no_mod_comment'], '
    '; 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 e6a3c6815e..26c01306d4 100644 --- a/Themes/default/css/index.css +++ b/Themes/default/css/index.css @@ -457,6 +457,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; @@ -1477,7 +1501,7 @@ ul li.greeting { margin: 1em 0 2em; } #post_event .roundframe { - padding: 12px 12%; + padding: 12px 8%; overflow: auto; } #post_event fieldset { @@ -3544,7 +3568,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; } @@ -3554,8 +3578,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 13d635180d..8ba4a27753 100644 --- a/Themes/default/css/jquery.sceditor.default.css +++ b/Themes/default/css/jquery.sceditor.default.css @@ -105,8 +105,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/images/bbc/heading.png b/Themes/default/images/bbc/heading.png new file mode 100644 index 0000000000000000000000000000000000000000..6bcc65efb50c8d7a584525ba7ece8aa7dcb2d5e9 GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE({hRn3dtu2NdBf@Q5sC zV9-+rVaAH3_GLi9Y)==*5R21yCk65yP~dQmew_N%A!hq;N$z0tpLg%|XSxP89eBP_ zCAa&;%03mw1xya_|L!u_>}mbZo!=^Rt)|e|Rp;)D_kR+zpQmivC1HDd?ZK>jvQIUO ttPbv&H&?FKD=4jZ^~5te-zV0cW4zj*az#CG;sT(x44$rjF6*2UngDC}N45X} literal 0 HcmV?d00001 diff --git a/Themes/default/scripts/jquery.sceditor.smf.js b/Themes/default/scripts/jquery.sceditor.smf.js index 79910a52e0..1dceea38e6 100644 --- a/Themes/default/scripts/jquery.sceditor.smf.js +++ b/Themes/default/scripts/jquery.sceditor.smf.js @@ -851,6 +851,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: '' @@ -1642,4 +1719,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 From 16838032af201547468100125c029a6901046ae0 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Mon, 3 Jun 2024 11:45:21 -0600 Subject: [PATCH 06/18] Moves hard-coded tab substitute string into a constant Also changes the content of the tab substitute string to something better and unique. Signed-off-by: Jon Stovell --- Sources/Actions/Admin/ErrorLog.php | 4 ++-- Sources/BBCodeParser.php | 2 +- Sources/Utils.php | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Sources/Actions/Admin/ErrorLog.php b/Sources/Actions/Admin/ErrorLog.php index f8b8684095..b5a5664be9 100644 --- a/Sources/Actions/Admin/ErrorLog.php +++ b/Sources/Actions/Admin/ErrorLog.php @@ -318,11 +318,11 @@ public function view(): void } elseif ($this->filter['variable'] == 'url') { Utils::$context['filter']['value']['html'] = '\'' . strtr(Utils::htmlspecialchars((str_starts_with($this->filter['value']['sql'], '?') ? Config::$scripturl : '') . $this->filter['value']['sql']), ['\\_' => '_']) . '\''; } elseif ($this->filter['variable'] == 'message') { - Utils::$context['filter']['value']['html'] = '\'' . strtr(Utils::htmlspecialchars($this->filter['value']['sql']), ["\n" => '
    ', '<br />' => '
    ', "\t" => '   ', '\\_' => '_', '\\%' => '%', '\\\\' => '\\']) . '\''; + Utils::$context['filter']['value']['html'] = '\'' . strtr(Utils::htmlspecialchars($this->filter['value']['sql']), ["\n" => '
    ', '<br />' => '
    ', "\t" => Utils::TAB_SUBSTITUTE, '\\_' => '_', '\\%' => '%', '\\\\' => '\\']) . '\''; Utils::$context['filter']['value']['html'] = preg_replace('~&lt;span class=&quot;remove&quot;&gt;(.+?)&lt;/span&gt;~', '$1', Utils::$context['filter']['value']['html']); } elseif ($this->filter['variable'] == 'error_type') { - Utils::$context['filter']['value']['html'] = '\'' . strtr(Utils::htmlspecialchars($this->filter['value']['sql']), ["\n" => '
    ', '<br />' => '
    ', "\t" => '   ', '\\_' => '_', '\\%' => '%', '\\\\' => '\\']) . '\''; + Utils::$context['filter']['value']['html'] = '\'' . strtr(Utils::htmlspecialchars($this->filter['value']['sql']), ["\n" => '
    ', '<br />' => '
    ', "\t" => Utils::TAB_SUBSTITUTE, '\\_' => '_', '\\%' => '%', '\\\\' => '\\']) . '\''; } else { Utils::$context['filter']['value']['html'] = &$this->filter['value']['sql']; } diff --git a/Sources/BBCodeParser.php b/Sources/BBCodeParser.php index 7223a80e3c..5edb83e443 100644 --- a/Sources/BBCodeParser.php +++ b/Sources/BBCodeParser.php @@ -2648,7 +2648,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)) { diff --git a/Sources/Utils.php b/Sources/Utils.php index 6cd0452dbf..faacf7f7a0 100644 --- a/Sources/Utils.php +++ b/Sources/Utils.php @@ -222,6 +222,13 @@ class Utils */ public const ENT_NBSP = '&(?' . '>nbsp|#(?' . '>x0*A0|0*160));'; + /** + * @var string + * + * Used to force the browser not to collapse tabs. + */ + public const TAB_SUBSTITUTE = "\u{200B}\u{2007}\u{2007}\u{2007}\u{2007}\u{200B}"; + /************************** * Public static properties **************************/ From 01b2a4ce3321eb8d3e0e3b110fb05844098efb73 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Thu, 20 Jun 2024 15:18:38 -0600 Subject: [PATCH 07/18] Improves handling and display of tab characters in posts, etc. Signed-off-by: Jon Stovell --- Sources/Mail.php | 3 +++ Sources/ServerSideIncludes.php | 6 +++++- Sources/Utils.php | 8 ++++++++ Themes/default/css/index.css | 1 + 4 files changed, 17 insertions(+), 1 deletion(-) 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/ServerSideIncludes.php b/Sources/ServerSideIncludes.php index b46296d0dc..f361d9f959 100644 --- a/Sources/ServerSideIncludes.php +++ b/Sources/ServerSideIncludes.php @@ -524,6 +524,8 @@ public static function queryPosts( $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], $row['id_msg']); + $row['body'] = strtr($row['body'], [Utils::TAB_SUBSTITUTE => '' . "\t" . '']); + // Censor it! Lang::censorText($row['subject']); Lang::censorText($row['body']); @@ -702,7 +704,7 @@ 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'] = strip_tags(strtr(BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], $row['id_msg']), ['
    ' => ' ', Utils::TAB_SUBSTITUTE => ' '])); if (Utils::entityStrlen($row['body']) > 128) { $row['body'] = Utils::entitySubstr($row['body'], 0, 128) . '...'; @@ -2241,6 +2243,8 @@ public static function boardNews(?int $board = null, ?int $limit = null, ?int $s $row['body'] = BBCodeParser::load()->parse($row['body'], (bool) $row['smileys_enabled'], $row['id_msg']); + $row['body'] = strtr($row['body'], [Utils::TAB_SUBSTITUTE => '' . "\t" . '']); + if (!empty($recycle_board) && $row['id_board'] == $recycle_board) { $row['icon'] = 'recycled'; } diff --git a/Sources/Utils.php b/Sources/Utils.php index faacf7f7a0..93cc2cc00a 100644 --- a/Sources/Utils.php +++ b/Sources/Utils.php @@ -226,6 +226,11 @@ class Utils * @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}"; @@ -2348,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/Themes/default/css/index.css b/Themes/default/css/index.css index 26c01306d4..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; From 5620163b871e779e40dd53ad2999c39d7f742d89 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Sat, 22 Jun 2024 01:27:40 -0600 Subject: [PATCH 08/18] For the sake of Markdown, convinces SCEditor not to delete tabs Signed-off-by: Jon Stovell --- Sources/Editor.php | 2 - .../default/css/jquery.sceditor.default.css | 1 + Themes/default/scripts/jquery.sceditor.smf.js | 47 +++++++++++++++++-- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/Sources/Editor.php b/Sources/Editor.php index 3e4174c342..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. '[#]' => '[+]', diff --git a/Themes/default/css/jquery.sceditor.default.css b/Themes/default/css/jquery.sceditor.default.css index 8ba4a27753..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%; diff --git a/Themes/default/scripts/jquery.sceditor.smf.js b/Themes/default/scripts/jquery.sceditor.smf.js index 1dceea38e6..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; } @@ -1050,6 +1075,22 @@ sceditor.command.set( } ); +// 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: { From a04186a1ba0052e62fac8431d52384a9e7726b58 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Sun, 9 Jun 2024 11:43:39 -0600 Subject: [PATCH 09/18] Implements SMF\MarkdownParser Signed-off-by: Jon Stovell --- Languages/en_US/Admin.php | 9 +- Languages/en_US/Help.php | 4 + Sources/Actions/Admin/Features.php | 17 + Sources/BBCodeParser.php | 5 +- Sources/MarkdownParser.php | 4010 ++++++++++++++++++++++++++++ Sources/Theme.php | 4 + Themes/default/css/markdown.css | 105 + 7 files changed, 4150 insertions(+), 4 deletions(-) create mode 100644 Sources/MarkdownParser.php create mode 100644 Themes/default/css/markdown.css 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/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/Features.php b/Sources/Actions/Admin/Features.php index 4c6329c0c0..e7136c2c40 100644 --- a/Sources/Actions/Admin/Features.php +++ b/Sources/Actions/Admin/Features.php @@ -27,6 +27,7 @@ use SMF\IntegrationHook; use SMF\ItemList; use SMF\Lang; +use SMF\MarkdownParser; use SMF\Menu; use SMF\Profile; use SMF\Sapi; @@ -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'])) { @@ -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); @@ -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/BBCodeParser.php b/Sources/BBCodeParser.php index 5edb83e443..2103fa2f8a 100644 --- a/Sources/BBCodeParser.php +++ b/Sources/BBCodeParser.php @@ -443,7 +443,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', @@ -584,7 +585,7 @@ class BBCodeParser [ 'tag' => 'html', 'type' => 'unparsed_content', - 'content' => '
    $1
    ', + 'content' => '
    $1
    ', 'block_level' => true, 'disabled_content' => '$1', ], diff --git a/Sources/MarkdownParser.php b/Sources/MarkdownParser.php new file mode 100644 index 0000000000..b9399bd1a3 --- /dev/null +++ b/Sources/MarkdownParser.php @@ -0,0 +1,4010 @@ +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 = 0; + + /** + * @var int + * + * Possible value for $this->output_type. + * + * Used to set the output to HTML rendered like the equivalent BBCode. + * This is the default output type. + */ + public const OUTPUT_HTML = 1; + + /** + * @var int + * + * Possible value for $this->output_type. + * + * Used to convert Markdown into BBCode. + */ + public const OUTPUT_BBC = 2; + + /** + * @var int + * + * Possible value for $this->hard_breaks. + * + * Using this option, line breaks will be converted to
    elements when + * the line breaks create blank lines. This can be used to preserve blank + * lines in the input while still parsing paragraph content normally. + */ + public const BR_LINES = 0b01; + + /** + * @var int + * + * Possible value for $this->hard_breaks. + * + * Using this option, line breaks will be converted to
    elements inside + * paragraphs, etc. + */ + public const BR_IN_PARAGRAPHS = 0b10; + + /** + * @var array + * + * Characters that can be escaped with a backslash. + */ + public const ESCAPEABLE = [ + '!', '"', '#', '$', '%', '&', '\'', '\\', + '(', ')', '*', '+', ',', '-', '.', '/', + ':', ';', '<', '=', '>', '?', '@', '|', + '[', ']', '^', '_', '`', '{', '}', '~', + ]; + + /** + * @var string + * + * Regex to match HTML tags, including opening tags, closing tags, comments, + * processing instructions, declarations, and CDATA sections. Matches both + * standard HTML5 tag names and custom tag names. + */ + public const REGEX_HTML_TAG = + '(' . + '(?P>opening_tag)' . + '|' . + '(?P>closing_tag)' . + '|' . + '(?P>comment)' . + '|' . + '(?P>processing_instruction)' . + '|' . + '(?P>declaration)' . + '|' . + '(?P>cdata)' . + ')' . + '(?(DEFINE)' . + '(?[a-zA-Z][a-zA-Z0-9\-]*)' . + '(?[^\s"\'=<>`])' . + '(?\'[^\']*\')' . + '(?"[^"]*")' . + '(?(?P>attribute_value_unquoted)|(?P>attribute_value_single_quoted)|(?P>attribute_value_double_quoted))' . + '(?\s*=\s*(?P>attribute_value))' . + '(?[a-zA-Z_:][a-zA-Z0-9_.:\-]*)' . + '(?\s+(?P>attribute_name)(?P>attribute_value_specification)?)' . + '(?<(?P>tag_name)(?P>attribute)*\s*/?>)' . + '(?tag_name)\s*>)' . + '(?)' . + '(?<' . '\?\X*?\?' . '>)' . + '(?]+>)' . + '(?)' . + ')'; + + /** + * @var string + * + * Regular expression to match link text. + */ + public const REGEX_LINK_TEXT = + '(?P' . + // Opening bracket. + '\[' . + // Any number of... + '(?' . '>' . + // characters that are... + '(?' . '>' . + // not square brackets... + '[^\[\]]' . + // or + '|' . + // escaped square brackets... + '\\\\[\[\]]' . + ')' . + // or + '|' . + // balanced square brackets. + '(?P>text)' . + ')*' . + // Closing bracket. + '\]' . + ')'; + + /** + * @var string + * + * Regular expression to match link labels. + */ + public const REGEX_LINK_LABEL = + '(?P