diff --git a/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeImg.class.php b/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeImg.class.php index 448b96507b4..f00a67c7d23 100644 --- a/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeImg.class.php +++ b/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeImg.class.php @@ -44,7 +44,14 @@ public function process(array $elements, AbstractHtmlNodeProcessor $htmlNodeProc // Render the mapped unicode emoji instead of the legacy smiley image. if ($smiley !== null && $smiley->emoji !== '') { - $htmlNodeProcessor->replaceElementWithText($element, $smiley->emoji, false); + if ($this->outputType === 'text/html') { + $element->replaceWith( + HtmlOutputNodeProcessor::createEmojiElement($element->ownerDocument, $smiley->emoji) + ); + } else { + $htmlNodeProcessor->replaceElementWithText($element, $smiley->emoji, false); + } + continue; } diff --git a/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeProcessor.class.php b/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeProcessor.class.php index fa8a7c293eb..2a8522f2323 100644 --- a/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeProcessor.class.php +++ b/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeProcessor.class.php @@ -27,6 +27,34 @@ class HtmlOutputNodeProcessor extends AbstractHtmlNodeProcessor */ protected $nodeInterface = IHtmlOutputNode::class; + /** + * Matches a run of one or more consecutive emojis, including keycaps, flags, + * skin tone modifiers, tag sequences and ZWJ sequences. Pictographs with a + * default text presentation (e.g. `©` or `™`) only qualify when they are + * explicitly requested as an emoji through a variation selector or modifier. + */ + private const EMOJI_PATTERN = <<<'REGEX' + ~( + (?: + \p{Regional_Indicator}{2} + | [0-9\#*]\x{FE0F}?\x{20E3} + | (?:\p{Emoji_Presentation}|\p{Extended_Pictographic}(?=\x{FE0F}|\p{Emoji_Modifier})) + \x{FE0F}?\p{Emoji_Modifier}? + (?:[\x{E0020}-\x{E007E}]+\x{E007F})? + (?:\x{200D}\p{Extended_Pictographic}\x{FE0F}?\p{Emoji_Modifier}?)* + )+ + )~ux + REGEX; + + /** + * Cheap pre-check for code points that are part of an emoji. The full + * `EMOJI_PATTERN` is considerably slower and is only evaluated for text + * that contains at least one of these code points. Covers all code points + * with the `Extended_Pictographic` or `Emoji_Presentation` property while + * skipping common non-emoji ranges such as currency symbols and CJK text. + */ + private const EMOJI_CANDIDATE_PATTERN = '~[\x{A9}\x{AE}\x{203C}\x{2049}\x{20E3}\x{2122}-\x{2BFF}\x{3030}\x{303D}\x{3297}\x{3299}\x{1F000}-\x{1FFFF}]~u'; + /** * desired output type * @var string @@ -85,6 +113,10 @@ public function process() $this->highlightKeywords(); + if ($this->outputType === 'text/html') { + $this->wrapEmojis(); + } + $this->invokeHtmlNode(new HtmlOutputNodeWoltlabMetacode()); if (\MODULE_URL_UNFURLING !== 0) { @@ -264,6 +296,61 @@ protected function highlightKeywords() } } + /** + * Wraps emojis in a `` to render them at a larger size + * than the surrounding text. + */ + protected function wrapEmojis(): void + { + $nodes = []; + foreach ($this->getXPath()->query('//text()') as $node) { + \assert($node instanceof \DOMText); + $text = $node->textContent; + if (\preg_match(self::EMOJI_CANDIDATE_PATTERN, $text) !== 1) { + continue; + } + + if (\preg_match(self::EMOJI_PATTERN, $text) !== 1) { + continue; + } + + if ($this->hasCodeParent($node)) { + continue; + } + + $nodes[] = $node; + } + + foreach ($nodes as $node) { + $split = \preg_split(self::EMOJI_PATTERN, $node->textContent, -1, \PREG_SPLIT_DELIM_CAPTURE); + foreach ($split as $i => $value) { + if ($value === '') { + continue; + } + + if ($i % 2 === 0) { + $node->parentNode->insertBefore($node->ownerDocument->createTextNode($value), $node); + } else { + $node->parentNode->insertBefore(self::createEmojiElement($node->ownerDocument, $value), $node); + } + } + + DOMUtil::removeNode($node); + } + } + + /** + * Creates the element that is used to render one or more emojis. + */ + public static function createEmojiElement(\DOMDocument $document, string $emoji): \DOMElement + { + $element = $document->createElement('span'); + $element->setAttribute('class', 'emoji'); + $element->appendChild($document->createTextNode($emoji)); + + return $element; + } + /** * Returns true if text node is inside a code element, suppressing any * auto-detection of content. diff --git a/wcfsetup/install/files/style/layout/global.scss b/wcfsetup/install/files/style/layout/global.scss index aecb1e00e0c..3916aac66f9 100644 --- a/wcfsetup/install/files/style/layout/global.scss +++ b/wcfsetup/install/files/style/layout/global.scss @@ -121,6 +121,11 @@ ol.nativeList { max-width: 100%; } + .emoji { + font-size: 1.375em; + line-height: 1; + } + > :first-child { margin-top: 0 !important;