From 3d9f265d3bd3eb652a33a4d7457712919d0b8d93 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Tue, 13 Jun 2017 15:23:38 +0200 Subject: [PATCH] Added native `[html]` bbcode See #2301 --- com.woltlab.wcf/bbcode.xml | 8 + com.woltlab.wcf/templates/wysiwyg.tpl | 5 + com.woltlab.wcf/templates/wysiwygToolbar.tpl | 13 +- .../acp/templates/bbCodeSelectOptionType.tpl | 2 +- .../install/files/acp/templates/header.tpl | 7 +- .../files/acp/templates/userGroupAdd.tpl | 8 + .../3rdParty/redactor2/plugins/WoltLabHtml.js | 11 ++ .../js/WoltLabSuite/Core/Ui/Redactor/Code.js | 11 +- .../js/WoltLabSuite/Core/Ui/Redactor/Html.js | 169 ++++++++++++++++++ .../lib/acp/form/UserGroupEditForm.class.php | 4 +- .../lib/system/bbcode/HtmlBBCode.class.php | 26 +++ .../builder/OptionCacheBuilder.class.php | 9 +- .../node/HtmlInputNodeProcessor.class.php | 10 +- .../converter/HtmlMetacodeConverter.class.php | 63 +++++++ .../output/node/HtmlOutputNodePre.class.php | 13 ++ .../BBCodeSelectUserGroupOptionType.class.php | 2 - wcfsetup/install/files/style/bbcode/code.scss | 21 ++- wcfsetup/install/lang/de.xml | 4 + wcfsetup/install/lang/en.xml | 4 + 19 files changed, 374 insertions(+), 16 deletions(-) create mode 100644 wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabHtml.js create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Html.js create mode 100644 wcfsetup/install/files/lib/system/bbcode/HtmlBBCode.class.php create mode 100644 wcfsetup/install/files/lib/system/html/metacode/converter/HtmlMetacodeConverter.class.php diff --git a/com.woltlab.wcf/bbcode.xml b/com.woltlab.wcf/bbcode.xml index 70977a2b88..d3dd5d6ee7 100644 --- a/com.woltlab.wcf/bbcode.xml +++ b/com.woltlab.wcf/bbcode.xml @@ -228,5 +228,13 @@ + + + wcf\system\bbcode\HtmlBBCode + 1 + 1 + fa-html5 + wcf.editor.button.woltlabHtml + diff --git a/com.woltlab.wcf/templates/wysiwyg.tpl b/com.woltlab.wcf/templates/wysiwyg.tpl index 5b8d25f6f7..c4293b7bc2 100644 --- a/com.woltlab.wcf/templates/wysiwyg.tpl +++ b/com.woltlab.wcf/templates/wysiwyg.tpl @@ -24,6 +24,7 @@ '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabEvent.js?v={@LAST_UPDATE_TIME}', '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabFont.js?v={@LAST_UPDATE_TIME}', '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabFullscreen.js?v={@LAST_UPDATE_TIME}', + '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabHtml.js?v={@LAST_UPDATE_TIME}', '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabImage.js?v={@LAST_UPDATE_TIME}', '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabInlineCode.js?v={@LAST_UPDATE_TIME}', '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabInsert.js?v={@LAST_UPDATE_TIME}', @@ -71,6 +72,9 @@ 'wcf.editor.code.line.description': '{lang}wcf.editor.code.line.description{/lang}', 'wcf.editor.code.title': '{lang __literal=true}wcf.editor.code.title{/lang}', + 'wcf.editor.html.description': '{lang}wcf.editor.html.description{/lang}', + 'wcf.editor.html.title': '{lang}wcf.editor.html.title{/lang}', + 'wcf.editor.image.edit': '{lang}wcf.editor.image.edit{/lang}', 'wcf.editor.image.insert': '{lang}wcf.editor.image.insert{/lang}', 'wcf.editor.image.link': '{lang}wcf.editor.image.link{/lang}', @@ -201,6 +205,7 @@ 'WoltLabDropdown', {if $__wcf->getBBCodeHandler()->isAvailableBBCode('font')}'WoltLabFont',{/if} 'WoltLabFullscreen', + {if $__wcf->getBBCodeHandler()->isAvailableBBCode('html')}'WoltLabHtml',{/if} {if $__wcf->getBBCodeHandler()->isAvailableBBCode('img')}'WoltLabImage',{/if} 'WoltLabInlineCode', 'WoltLabInsert', diff --git a/com.woltlab.wcf/templates/wysiwygToolbar.tpl b/com.woltlab.wcf/templates/wysiwygToolbar.tpl index 1e5a21ab84..642d5076d3 100644 --- a/com.woltlab.wcf/templates/wysiwygToolbar.tpl +++ b/com.woltlab.wcf/templates/wysiwygToolbar.tpl @@ -78,7 +78,14 @@ buttons.push('wcfSeparator'); buttons.push('woltlabQuote'); {foreach from=$__wcf->getBBCodeHandler()->getButtonBBCodes(true) item=__bbcode} - buttonOptions['{$__bbcode->bbcodeTag}'] = { icon: '{$__bbcode->wysiwygIcon}', title: '{lang}{$__bbcode->buttonLabel}{/lang}' }; - buttons.push('{$__bbcode->bbcodeTag}'); - customButtons.push('{$__bbcode->bbcodeTag}'); + {* the HTML bbcode must be handled differently, it conflicts with the `source` toggle-button *} + {if $__bbcode->bbcodeTag === 'html'} + buttonOptions['woltlabHtml'] = { icon: '{$__bbcode->wysiwygIcon}', title: '{lang}{$__bbcode->buttonLabel}{/lang}' }; + buttons.push('woltlabHtml'); + customButtons.push('woltlabHtml'); + {else} + buttonOptions['{$__bbcode->bbcodeTag}'] = { icon: '{$__bbcode->wysiwygIcon}', title: '{lang}{$__bbcode->buttonLabel}{/lang}' }; + buttons.push('{$__bbcode->bbcodeTag}'); + customButtons.push('{$__bbcode->bbcodeTag}'); + {/if} {/foreach} diff --git a/wcfsetup/install/files/acp/templates/bbCodeSelectOptionType.tpl b/wcfsetup/install/files/acp/templates/bbCodeSelectOptionType.tpl index 1645317f1b..d0eb58a96c 100644 --- a/wcfsetup/install/files/acp/templates/bbCodeSelectOptionType.tpl +++ b/wcfsetup/install/files/acp/templates/bbCodeSelectOptionType.tpl @@ -1,3 +1,3 @@ {foreach from=$bbCodes item='bbCode'} - + {$bbCode} {/foreach} \ No newline at end of file diff --git a/wcfsetup/install/files/acp/templates/header.tpl b/wcfsetup/install/files/acp/templates/header.tpl index adf37670ab..78408f15b2 100644 --- a/wcfsetup/install/files/acp/templates/header.tpl +++ b/wcfsetup/install/files/acp/templates/header.tpl @@ -12,8 +12,11 @@ {event name='stylesheets'} - - + + + + + diff --git a/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabHtml.js b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabHtml.js new file mode 100644 index 0000000000..0c2f9e4935 --- /dev/null +++ b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabHtml.js @@ -0,0 +1,11 @@ +$.Redactor.prototype.WoltLabHtml = function() { + "use strict"; + + return { + init: function() { + require(['WoltLabSuite/Core/Ui/Redactor/Html'], (function (UiRedactorHtml) { + new UiRedactorHtml(this); + }).bind(this)); + } + }; +}; \ No newline at end of file diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Code.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Code.js index 4b6952a5ee..e1c3cfdbb4 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Code.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Code.js @@ -64,10 +64,15 @@ define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Di _bbcodeCode: function(data) { data.cancel = true; + var pre = this._editor.selection.block(); + if (pre && pre.nodeName === 'PRE' && pre.classList.contains('woltlabHtml')) { + return; + } + this._editor.button.toggle({}, 'pre', 'func', 'block.format'); - var pre = this._editor.selection.block(); - if (pre && pre.nodeName === 'PRE') { + pre = this._editor.selection.block(); + if (pre && pre.nodeName === 'PRE' && !pre.classList.contains('woltlabHtml')) { if (pre.childElementCount === 1 && pre.children[0].nodeName === 'BR') { // drop superfluous linebreak pre.removeChild(pre.children[0]); @@ -89,7 +94,7 @@ define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Di * @protected */ _observeLoad: function() { - elBySelAll('pre', this._editor.$editor[0], (function(pre) { + elBySelAll('pre:not(.woltlabHtml)', this._editor.$editor[0], (function(pre) { pre.addEventListener('mousedown', this._callbackEdit); this._setTitle(pre); }).bind(this)); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Html.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Html.js new file mode 100644 index 0000000000..4a2f821059 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Html.js @@ -0,0 +1,169 @@ +/** + * Manages html code blocks. + * + * @author Alexander Ebert + * @copyright 2001-2017 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Ui/Redactor/Html + */ +define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog', './PseudoHeader'], function (EventHandler, EventKey, Language, StringUtil, DomUtil, UiDialog, UiRedactorPseudoHeader) { + "use strict"; + + if (!COMPILER_TARGET_DEFAULT) { + var Fake = function() {}; + Fake.prototype = { + init: function() {}, + _bbcodeCode: function() {}, + _observeLoad: function() {}, + _edit: function() {}, + _save: function() {}, + _setTitle: function() {}, + _delete: function() {}, + _dialogSetup: function() {} + }; + return Fake; + } + + var _headerHeight = 0; + + /** + * @param {Object} editor editor instance + * @constructor + */ + function UiRedactorHtml(editor) { this.init(editor); } + UiRedactorHtml.prototype = { + /** + * Initializes the source code management. + * + * @param {Object} editor editor instance + */ + init: function(editor) { + this._editor = editor; + this._elementId = this._editor.$element[0].id; + this._pre = null; + + EventHandler.add('com.woltlab.wcf.redactor2', 'bbcode_woltlabHtml_' + this._elementId, this._bbcodeCode.bind(this)); + EventHandler.add('com.woltlab.wcf.redactor2', 'observe_load_' + this._elementId, this._observeLoad.bind(this)); + + // support for active button marking + this._editor.opts.activeButtonsStates['pre-html'] = 'code'; + + // static bind to ensure that removing works + this._callbackEdit = this._edit.bind(this); + + // bind listeners on init + this._observeLoad(); + }, + + /** + * Intercepts the insertion of `[woltlabHtml]` tags and uses a native `
` instead.
+		 *
+		 * @param       {Object}        data    event data
+		 * @protected
+		 */
+		_bbcodeCode: function(data) {
+			data.cancel = true;
+			
+			var pre = this._editor.selection.block();
+			if (pre && pre.nodeName === 'PRE' && !pre.classList.contains('woltlabHtml')) {
+				return;
+			}
+			
+			this._editor.button.toggle({}, 'pre', 'func', 'block.format');
+			
+			pre = this._editor.selection.block();
+			if (pre && pre.nodeName === 'PRE') {
+				pre.classList.add('woltlabHtml');
+				
+				if (pre.childElementCount === 1 && pre.children[0].nodeName === 'BR') {
+					// drop superfluous linebreak
+					pre.removeChild(pre.children[0]);
+				}
+				
+				this._setTitle(pre);
+				
+				pre.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
+				
+				// work-around for Safari
+				this._editor.caret.end(pre);
+			}
+		},
+		
+		/**
+		 * Binds event listeners and sets quote title on both editor
+		 * initialization and when switching back from code view.
+		 *
+		 * @protected
+		 */
+		_observeLoad: function() {
+			elBySelAll('pre.woltlabHtml', this._editor.$editor[0], (function(pre) {
+				pre.addEventListener('mousedown', this._callbackEdit);
+				this._setTitle(pre);
+			}).bind(this));
+		},
+		
+		/**
+		 * Opens the dialog overlay to edit the code's properties.
+		 *
+		 * @param       {Event}         event           event object
+		 * @protected
+		 */
+		_edit: function(event) {
+			var pre = event.currentTarget;
+			
+			if (_headerHeight === 0) {
+				_headerHeight = UiRedactorPseudoHeader.getHeight(pre);
+			}
+			
+			// check if the click hit the header
+			var offset = DomUtil.offset(pre);
+			if (event.pageY > offset.top && event.pageY < (offset.top + _headerHeight)) {
+				event.preventDefault();
+				
+				this._editor.selection.save();
+				this._pre = pre;
+				
+				console.warn("should edit");
+			}
+		},
+		
+		/**
+		 * Sets or updates the code's header title.
+		 *
+		 * @param       {Element}       pre     code element
+		 * @protected
+		 */
+		_setTitle: function(pre) {
+			['title', 'description'].forEach(function(title) {
+				var phrase = Language.get('wcf.editor.html.' + title);
+				
+				if (elData(pre, title) !== phrase) {
+					elData(pre, title, phrase);
+				}
+			});
+		},
+		
+		_delete: function (event) {
+			console.warn("should delete");
+			event.preventDefault();
+			
+			var caretEnd = this._pre.nextElementSibling || this._pre.previousElementSibling;
+			if (caretEnd === null && this._pre.parentNode !== this._editor.core.editor()[0]) {
+				caretEnd = this._pre.parentNode;
+			}
+			
+			if (caretEnd === null) {
+				this._editor.code.set('');
+				this._editor.focus.end();
+			}
+			else {
+				elRemove(this._pre);
+				this._editor.caret.end(caretEnd);
+			}
+			
+			UiDialog.close(this);
+		}
+	};
+	
+	return UiRedactorHtml;
+});
\ No newline at end of file
diff --git a/wcfsetup/install/files/lib/acp/form/UserGroupEditForm.class.php b/wcfsetup/install/files/lib/acp/form/UserGroupEditForm.class.php
index 8b764fb7a7..fd5a2e06ff 100755
--- a/wcfsetup/install/files/lib/acp/form/UserGroupEditForm.class.php
+++ b/wcfsetup/install/files/lib/acp/form/UserGroupEditForm.class.php
@@ -102,7 +102,9 @@ class UserGroupEditForm extends UserGroupAddForm {
 			'group' => $this->group,
 			'action' => 'edit',
 			'availableUserGroups' => UserGroup::getAccessibleGroups(),
-			'groupIsGuest' => $this->group->groupType == UserGroup::GUESTS
+			'groupIsEveryone' => $this->group->groupType == UserGroup::EVERYONE,
+			'groupIsGuest' => $this->group->groupType == UserGroup::GUESTS,
+			'groupIsUsers' => $this->group->groupType == UserGroup::USERS
 		]);
 		
 		// add warning when the initiator is in the group
diff --git a/wcfsetup/install/files/lib/system/bbcode/HtmlBBCode.class.php b/wcfsetup/install/files/lib/system/bbcode/HtmlBBCode.class.php
new file mode 100644
index 0000000000..549bd4dde4
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/bbcode/HtmlBBCode.class.php
@@ -0,0 +1,26 @@
+
+ * @package	WoltLabSuite\Core\System\Bbcode
+ */
+class HtmlBBCode extends AbstractBBCode {
+	/**
+	 * @inheritDoc
+	 */
+	public function getParsedTag(array $openingTag, $content, array $closingTag, BBCodeParser $parser) {
+		$email = '';
+		if (isset($openingTag['attributes'][0])) {
+			$email = $openingTag['attributes'][0];
+		}
+		$email = StringUtil::decodeHTML($email);
+		
+		return '' . StringUtil::encodeHTML($email) . '';
+	}
+}
diff --git a/wcfsetup/install/files/lib/system/cache/builder/OptionCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/OptionCacheBuilder.class.php
index 98cc7cff27..b2cc84ba67 100644
--- a/wcfsetup/install/files/lib/system/cache/builder/OptionCacheBuilder.class.php
+++ b/wcfsetup/install/files/lib/system/cache/builder/OptionCacheBuilder.class.php
@@ -64,8 +64,13 @@ class OptionCacheBuilder extends AbstractCacheBuilder {
 		$statement = WCF::getDB()->prepareStatement($sql);
 		$statement->execute();
 		
-		/** @var Option $option */
-		while ($option = $statement->fetchObject($this->optionClassName)) {
+		while ($row = $statement->fetchArray()) {
+			if ($row['optionType'] === 'BBCodeSelect') {
+				$row['defaultValue'] = (!empty($row['defaultValue']) ? ',' : '') . 'html';
+			}
+			
+			/** @var Option $option */
+			$option = new $this->optionClassName(null, $row);
 			$data['options'][$option->optionName] = $option;
 			if (!isset($data['optionToCategories'][$option->categoryName])) {
 				$data['optionToCategories'][$option->categoryName] = [];
diff --git a/wcfsetup/install/files/lib/system/html/input/node/HtmlInputNodeProcessor.class.php b/wcfsetup/install/files/lib/system/html/input/node/HtmlInputNodeProcessor.class.php
index 4f3c07a543..11bfd0bc89 100644
--- a/wcfsetup/install/files/lib/system/html/input/node/HtmlInputNodeProcessor.class.php
+++ b/wcfsetup/install/files/lib/system/html/input/node/HtmlInputNodeProcessor.class.php
@@ -34,6 +34,7 @@ class HtmlInputNodeProcessor extends AbstractHtmlNodeProcessor {
 		],
 		'li' => ['text-center', 'text-justify', 'text-right'],
 		'p' => ['text-center', 'text-justify', 'text-right'],
+		'pre' => ['woltlabHtml'],
 		'td' => ['text-center', 'text-justify', 'text-right']
 	];
 	
@@ -104,6 +105,9 @@ class HtmlInputNodeProcessor extends AbstractHtmlNodeProcessor {
 		$textParser = new HtmlInputNodeTextParser($this, $smileyCount);
 		$textParser->parse();
 		
+		// handle HTML bbcode
+		$allowHtml = BBCodeHandler::getInstance()->isAvailableBBCode('html');
+		
 		// strip invalid class names
 		/** @var \DOMElement $element */
 		foreach ($this->getXPath()->query('//*[@class]') as $element) {
@@ -114,7 +118,11 @@ class HtmlInputNodeProcessor extends AbstractHtmlNodeProcessor {
 				}
 				
 				$classNames = explode(' ', $element->getAttribute('class'));
-				$classNames = array_filter($classNames, function ($className) use ($nodeName) {
+				$classNames = array_filter($classNames, function ($className) use ($allowHtml, $nodeName) {
+					if (!$allowHtml && $nodeName === 'pre' && $className === 'woltlabHtml') {
+						return false;
+					}
+					
 					return ($className && in_array($className, self::$allowedClassNames[$nodeName]));
 				});
 				
diff --git a/wcfsetup/install/files/lib/system/html/metacode/converter/HtmlMetacodeConverter.class.php b/wcfsetup/install/files/lib/system/html/metacode/converter/HtmlMetacodeConverter.class.php
new file mode 100644
index 0000000000..06e8153c03
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/html/metacode/converter/HtmlMetacodeConverter.class.php
@@ -0,0 +1,63 @@
+`.
+ * 
+ * @author      Alexander Ebert
+ * @copyright   2001-2017 WoltLab GmbH
+ * @license     GNU Lesser General Public License 
+ * @package     WoltLabSuite\Core\System\Html\Metacode\Converter
+ * @since       3.0
+ */
+class HtmlMetacodeConverter extends AbstractMetacodeConverter {
+	/**
+	 * @inheritDoc
+	 */
+	public function convert(\DOMDocumentFragment $fragment, array $attributes) {
+		$element = $fragment->ownerDocument->createElement('pre');
+		$element->setAttribute('class', 'woltlabHtml');
+		$element->appendChild($fragment);
+		
+		// convert code lines
+		$childNodes = DOMUtil::getChildNodes($element);
+		/** @var \DOMElement $node */
+		foreach ($childNodes as $node) {
+			if ($node->nodeType === XML_ELEMENT_NODE && $node->nodeName === 'p') {
+				DOMUtil::insertAfter($node->ownerDocument->createTextNode("\n"), $node);
+				
+				$brs = $node->getElementsByTagName('br');
+				while ($brs->length) {
+					$br = $brs->item(0);
+					DOMUtil::insertBefore($br->ownerDocument->createTextNode("\n"), $br);
+					DOMUtil::removeNode($br);
+				}
+				
+				DOMUtil::removeNode($node, true);
+			}
+		}
+		
+		// clear any other elements contained within
+		$elements = $element->getElementsByTagName('*');
+		while ($elements->length) {
+			/** @var \DOMElement $child */
+			$child = $elements->item(0);
+			if ($child->nodeName === 'a') {
+				DOMUtil::insertBefore($child->ownerDocument->createTextNode($child->getAttribute('href')), $child);
+				DOMUtil::removeNode($child);
+				continue;
+			}
+			
+			DOMUtil::removeNode($child, true);
+		}
+		
+		// trim code block
+		$content = StringUtil::trim($element->textContent);
+		$element->nodeValue = '';
+		$element->appendChild($element->ownerDocument->createTextNode($content));
+		
+		return $element;
+	}
+}
diff --git a/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodePre.class.php b/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodePre.class.php
index 81ef19923d..8e931f380e 100644
--- a/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodePre.class.php
+++ b/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodePre.class.php
@@ -45,6 +45,14 @@ class HtmlOutputNodePre extends AbstractHtmlOutputNode {
 	public function process(array $elements, AbstractHtmlNodeProcessor $htmlNodeProcessor) {
 		/** @var \DOMElement $element */
 		foreach ($elements as $element) {
+			if ($element->getAttribute('class') === 'woltlabHtml') {
+				$nodeIdentifier = StringUtil::getRandomID();
+				$htmlNodeProcessor->addNodeData($this, $nodeIdentifier, ['rawHTML' => $element->textContent]);
+				
+				$htmlNodeProcessor->renameTag($element, 'wcfNode-' . $nodeIdentifier);
+				continue;
+			}
+			
 			switch ($this->outputType) {
 				case 'text/html':
 					$nodeIdentifier = StringUtil::getRandomID();
@@ -74,6 +82,11 @@ class HtmlOutputNodePre extends AbstractHtmlOutputNode {
 	 * @inheritDoc
 	 */
 	public function replaceTag(array $data) {
+		// HTML bbcode
+		if (isset($data['rawHTML'])) {
+			return $data['rawHTML'];
+		}
+		
 		$content = preg_replace('/^\s*\n/', '', $data['content']);
 		$content = preg_replace('/\n\s*$/', '', $content);
 		
diff --git a/wcfsetup/install/files/lib/system/option/user/group/BBCodeSelectUserGroupOptionType.class.php b/wcfsetup/install/files/lib/system/option/user/group/BBCodeSelectUserGroupOptionType.class.php
index 9d98abaea6..4193e68f18 100644
--- a/wcfsetup/install/files/lib/system/option/user/group/BBCodeSelectUserGroupOptionType.class.php
+++ b/wcfsetup/install/files/lib/system/option/user/group/BBCodeSelectUserGroupOptionType.class.php
@@ -58,8 +58,6 @@ class BBCodeSelectUserGroupOptionType extends AbstractOptionType implements IUse
 	
 	/**
 	 * Loads the list of BBCodes for the HTML select element.
-	 * 
-	 * @return	string[]
 	 */
 	protected function loadBBCodeSelection() {
 		$this->bbCodes = array_keys(BBCodeCache::getInstance()->getBBCodes());
diff --git a/wcfsetup/install/files/style/bbcode/code.scss b/wcfsetup/install/files/style/bbcode/code.scss
index 9173260184..ab2f3b7ed8 100644
--- a/wcfsetup/install/files/style/bbcode/code.scss
+++ b/wcfsetup/install/files/style/bbcode/code.scss
@@ -11,7 +11,8 @@
 	word-break: break-all;
 	word-wrap: break-word;
 	
-	&:not(.redactorCalcHeight)::before {
+	&:not(.redactorCalcHeight)::before,
+	&.woltlabHtml::before {
 		color: $wcfContentLink;
 		content: attr(data-title);
 		cursor: pointer;
@@ -21,6 +22,24 @@
 		
 		@include wcfFontHeadline;
 	}
+	
+	&.woltlabHtml {
+		&::before {
+			margin-bottom: 30px;
+		}
+		
+		&::after {
+			color: $wcfContentDimmedText;
+			content: attr(data-description);
+			cursor: pointer;
+			display: block;
+			font-family: $wcfFontFamily;
+			position: absolute;
+			top: 32px;
+			
+			@include wcfFontSmall;
+		}
+	}
 }
 
 .codeBox {
diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml
index b07e2fb684..d09dbe79ab 100644
--- a/wcfsetup/install/lang/de.xml
+++ b/wcfsetup/install/lang/de.xml
@@ -2554,6 +2554,7 @@ Fehler sind beispielsweise:
 		
 		
 		
+		
 		
 		
 		
@@ -2570,6 +2571,9 @@ Fehler sind beispielsweise:
 		
 		
 		
+		
+		
+		
 		
 		
 		
diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml
index dc85a0dd47..e35c846a73 100644
--- a/wcfsetup/install/lang/en.xml
+++ b/wcfsetup/install/lang/en.xml
@@ -2497,6 +2497,7 @@ Errors are:
 		
 		
 		
+		
 		
 		
 		
@@ -2513,6 +2514,9 @@ Errors are:
 		
 		
 		
+		
+		
+		
 		
 		
 		
-- 
2.20.1