diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6270d1747f..bc1ec76c35 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1872,12 +1872,6 @@ parameters: count: 2 path: wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeImg.class.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodePre.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed diff --git a/wcfsetup/install/files/lib/system/code/SourceCodeRenderer.class.php b/wcfsetup/install/files/lib/system/code/SourceCodeRenderer.class.php new file mode 100644 index 0000000000..f293bee04a --- /dev/null +++ b/wcfsetup/install/files/lib/system/code/SourceCodeRenderer.class.php @@ -0,0 +1,251 @@ + + * @since 6.3 + */ +class SourceCodeRenderer +{ + /** + * aliases for highlighters that are not known by the client-side highlighter + * @var array + */ + private const HIGHLIGHTER_ALIASES = [ + 'js' => 'javascript', + 'c++' => 'cpp', + 'tex' => 'latex', + 'shell' => 'bash', + ]; + + /** + * already used ids for line numbers to prevent duplicate ids in the output + * @var array + */ + private static array $codeIDs = []; + + /** + * Renders the given source code. If no highlighter is provided, the used + * language is guessed based on the content. + */ + public function render( + string $content, + string $highlighter = '', + string $filename = '', + int $startLineNumber = 1, + string $codeIDPrefix = '' + ): string { + $content = $this->trimContent($content); + + if ($startLineNumber < 1) { + $startLineNumber = 1; + } + + $highlighter = $this->getHighlighter($content, $highlighter); + + $meta = BBCodeHandler::getInstance()->getHighlighterMeta(); + $title = WCF::getLanguage()->get('wcf.bbcode.code'); + if (isset($meta[$highlighter])) { + $title = $meta[$highlighter]['title']; + } else { + $highlighter = ''; + } + + $lines = $this->splitLines($content); + + return WCF::getTPL()->render('wcf', 'shared_codeMetaCode', [ + 'codeID' => $this->getCodeID($codeIDPrefix, $content), + 'startLineNumber' => $startLineNumber, + 'content' => $lines, + 'language' => $highlighter, + 'filename' => $filename, + 'title' => $title, + 'lines' => \count($lines), + ]); + } + + /** + * Removes a leading and a trailing empty line from the given content. + */ + public function trimContent(string $content): string + { + $content = \preg_replace('/^\s*\n/', '', $content); + + return \preg_replace('/\n\s*$/', '', $content); + } + + /** + * Returns the highlighter that should be used for the given content. The + * highlighter is guessed if no highlighter is provided. + */ + public function getHighlighter(string $content, string $highlighter = ''): string + { + $highlighter = $this->normalizeHighlighter($highlighter); + if ($highlighter === '') { + $highlighter = $this->guessHighlighter($content); + } + + return $highlighter; + } + + /** + * Resolves known aliases of highlighter names. + */ + public function normalizeHighlighter(string $highlighter): string + { + return self::HIGHLIGHTER_ALIASES[$highlighter] ?? $highlighter; + } + + /** + * Splits the content into single lines while preserving the line breaks. + * + * @return string[] + */ + public function splitLines(string $content): array + { + $lines = \explode("\n", $content); + $last = \array_pop($lines); + $lines = \array_map(static fn (string $line) => $line . "\n", $lines); + $lines[] = $last; + + return $lines; + } + + /** + * Returns a likely highlighter for the given content. + */ + public function guessHighlighter(string $content): string + { + // PHP at the beginning is almost surely PHP. + if (\str_starts_with($content, 'match($content) !== 0 + ) { + return 'python'; + } + + if (Regex::compile('^#!(/usr)?/bin/(ba|z)?sh')->match($content) !== 0) { + return 'bash'; + } + + if ( + \str_starts_with($content, 'FROM') + && \str_contains($content, "RUN") + ) { + return 'docker'; + } + + if ( + \stripos($content, "RewriteRule") !== false + || \stripos($content, "RewriteEngine On") !== false + || \stripos($content, "AuthUserFile") !== false + ) { + return 'apacheconf'; + } + + if (\str_contains($content, '\\documentclass')) { + return 'latex'; + } + + // PHP somewhere later might not necessarily be PHP, it could also be + // a .patch or a Dockerfile. + if (\str_contains($content, ' BooleanFormField::create('required') ->label('wcf.form.option.shared.required') ->value(false), + 'sourceCodeLanguage' => SingleSelectionFormField::create('sourceCodeLanguage') + ->label('wcf.form.option.shared.sourceCodeLanguage') + ->description('wcf.form.option.shared.sourceCodeLanguage.description') + ->options($this->getSourceCodeLanguageOptions(), labelLanguageItems: false) + ->filterable() + ->nullable(), 'unit' => TextFormField::create('unit') ->label('wcf.form.option.shared.unit') ->addFieldClass('short'), @@ -82,6 +91,23 @@ private function getDefaultFormFields(): array ]; } + /** + * Returns the list of available syntax highlighters. + * + * @return array + */ + private function getSourceCodeLanguageOptions(): array + { + $options = []; + foreach (BBCodeHandler::getInstance()->getHighlighterMeta() as $identifier => $data) { + $options[$identifier] = $data['title'] . (\strtolower($data['title']) !== $identifier ? ' (' . $identifier . ')' : ''); + } + + \asort($options); + + return ['' => WCF::getLanguage()->get('wcf.global.noSelection')] + $options; + } + /** * Returns a validator that ensures that the maximum value of a form field is not * smaller than the minimum value provided by the form field with the given id. diff --git a/wcfsetup/install/files/lib/system/form/option/SourceCodeFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/SourceCodeFormOption.class.php index ec1e5e74bf..994e0f591e 100644 --- a/wcfsetup/install/files/lib/system/form/option/SourceCodeFormOption.class.php +++ b/wcfsetup/install/files/lib/system/form/option/SourceCodeFormOption.class.php @@ -26,10 +26,23 @@ public function getId(): string return 'sourceCode'; } + #[\Override] + public function getConfigurationFormFields(): array + { + return \array_merge(parent::getConfigurationFormFields(), ['sourceCodeLanguage']); + } + #[\Override] public function getFormField(string $id, array $configuration = []): AbstractFormField { - return SourceCodeFormField::create($id); + $formField = SourceCodeFormField::create($id); + + $language = (string)($configuration['sourceCodeLanguage'] ?? ''); + if (\in_array($language, SourceCodeFormField::LANGUAGES, true)) { + $formField->language($language); + } + + return $formField; } #[\Override] diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/SourceCodeFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/SourceCodeFormatter.class.php index 7774050d48..399a9ef46b 100644 --- a/wcfsetup/install/files/lib/system/form/option/formatter/SourceCodeFormatter.class.php +++ b/wcfsetup/install/files/lib/system/form/option/formatter/SourceCodeFormatter.class.php @@ -2,7 +2,7 @@ namespace wcf\system\form\option\formatter; -use wcf\util\StringUtil; +use wcf\system\code\SourceCodeRenderer; /** * Formatter for source code values. @@ -17,6 +17,10 @@ final class SourceCodeFormatter implements IFormOptionFormatter #[\Override] public function format(string $value, int $languageID, array $configuration): string { - return '
' . StringUtil::encodeHTML($value) . '
'; + return (new SourceCodeRenderer())->render( + $value, + (string)($configuration['sourceCodeLanguage'] ?? ''), + codeIDPrefix: 'formOption_' + ); } } 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 f1c6d13c97..5d981e2a3c 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 @@ -2,10 +2,9 @@ namespace wcf\system\html\output\node; -use wcf\system\bbcode\BBCodeHandler; +use wcf\system\code\SourceCodeRenderer; use wcf\system\event\EventHandler; use wcf\system\html\node\AbstractHtmlNodeProcessor; -use wcf\system\Regex; use wcf\system\WCF; /** @@ -22,11 +21,7 @@ class HtmlOutputNodePre extends AbstractHtmlOutputNode */ protected $tagName = 'pre'; - /** - * already used ids for line numbers to prevent duplicate ids in the output - * @var string[] - */ - private static $codeIDs = []; + private ?SourceCodeRenderer $renderer = null; #[\Override] public function process(array $elements, AbstractHtmlNodeProcessor $htmlNodeProcessor) @@ -84,64 +79,28 @@ public function replaceTag(array $data) return $data['rawHTML']; } - $content = \preg_replace('/^\s*\n/', '', $data['content']); - $content = \preg_replace('/\n\s*$/', '', $content); + $content = $this->getRenderer()->trimContent($data['content']); + $highlighter = $this->getRenderer()->getHighlighter($content, $data['highlighter']); - $file = $data['file']; - $highlighter = $data['highlighter']; - $line = ($data['line'] < 1) ? 1 : $data['line']; - - switch ($highlighter) { - case 'js': - $highlighter = 'javascript'; - break; - case 'c++': - $highlighter = 'cpp'; - break; - case 'tex': - $highlighter = 'latex'; - break; - case 'shell': - $highlighter = 'bash'; - break; - } - - if (empty($highlighter)) { - $highlighter = $this->guessHighlighter($content); - } $eventData = [ 'highlighter' => $highlighter, 'data' => $data, 'content' => $content, ]; EventHandler::getInstance()->fireAction($this, 'selectHighlighter', $eventData); - $highlighter = $eventData['highlighter']; - $meta = BBCodeHandler::getInstance()->getHighlighterMeta(); - $title = WCF::getLanguage()->get('wcf.bbcode.code'); - if (isset($meta[$highlighter])) { - $title = $meta[$highlighter]['title']; - } else { - $highlighter = null; - } - - $splitContent = \explode("\n", $content); - $last = \array_pop($splitContent); - $splitContent = \array_map(static function ($item) { - return $item . "\n"; - }, $splitContent); - $splitContent[] = $last; + return $this->getRenderer()->render( + $content, + $eventData['highlighter'], + $data['file'], + (int)$data['line'], + $data['prefix'] ?? '' + ); + } - // show template - return WCF::getTPL()->render('wcf', 'shared_codeMetaCode', [ - 'codeID' => $this->getCodeID($data['prefix'] ?? '', $content), - 'startLineNumber' => $line, - 'content' => $splitContent, - 'language' => $highlighter, - 'filename' => $file, - 'title' => $title, - 'lines' => \count($splitContent), - ]); + private function getRenderer(): SourceCodeRenderer + { + return $this->renderer ??= new SourceCodeRenderer(); } /** @@ -151,111 +110,7 @@ public function replaceTag(array $data) */ public function guessHighlighter(string $content) { - // PHP at the beginning is almost surely PHP. - if (\str_starts_with($content, 'match($content) !== 0 - ) { - return 'python'; - } - - if (Regex::compile('^#!(/usr)?/bin/(ba|z)?sh')->match($content) !== 0) { - return 'bash'; - } - - if ( - \str_starts_with($content, 'FROM') - && \str_contains($content, "RUN") - ) { - return 'docker'; - } - - if ( - \stripos($content, "RewriteRule") !== false - || \stripos($content, "RewriteEngine On") !== false - || \stripos($content, "AuthUserFile") !== false - ) { - return 'apacheconf'; - } - - if (\str_contains($content, '\\documentclass')) { - return 'latex'; - } - - // PHP somewhere later might not necessarily be PHP, it could also be - // a .patch or a Dockerfile. - if (\str_contains($content, 'getRenderer()->guessHighlighter($content); } /** @@ -265,15 +120,6 @@ public function guessHighlighter(string $content) */ protected function getCodeID(string $prefix, string $code) { - $i = -1; - // find an unused codeID - do { - $codeID = $prefix . \mb_substr(\sha1($code), 0, 6) . (++$i !== 0 ? '_' . $i : ''); - } while (isset(self::$codeIDs[$codeID])); - - // mark codeID as used - self::$codeIDs[$codeID] = true; - - return $codeID; + return $this->getRenderer()->getCodeID($prefix, $code); } } diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 4b1fbaa48f..321fbe3250 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -4047,6 +4047,8 @@ Dateianhänge: + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 8fc0c952d4..5ca02385fc 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -3993,6 +3993,8 @@ Attachments: + +