Added proper support for code, quote and spoiler
authorAlexander Ebert <ebert@woltlab.com>
Thu, 16 Jun 2016 18:25:05 +0000 (20:25 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Thu, 16 Jun 2016 18:25:12 +0000 (20:25 +0200)
17 files changed:
com.woltlab.wcf/templates/codeMetaCode.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/spoilerMetaCode.tpl [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Code.js
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Quote.js
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Spoiler.js
wcfsetup/install/files/lib/system/bbcode/highlighter/BrainfuckHighlighter.class.php [deleted file]
wcfsetup/install/files/lib/system/html/input/filter/MessageHtmlInputFilter.class.php
wcfsetup/install/files/lib/system/html/metacode/converter/CodeMetacodeConverter.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/html/metacode/converter/SpoilerMetacodeConverter.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/html/node/HtmlNodeProcessor.class.php
wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeBlockquote.class.php
wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodePre.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeProcessor.class.php
wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeWoltlabSpoiler.class.php [new file with mode: 0644]
wcfsetup/install/files/style/bbcode/code.scss
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

diff --git a/com.woltlab.wcf/templates/codeMetaCode.tpl b/com.woltlab.wcf/templates/codeMetaCode.tpl
new file mode 100644 (file)
index 0000000..8fcf0ab
--- /dev/null
@@ -0,0 +1,33 @@
+<div class="codeBox collapsibleBbcode jsCollapsibleBbcode {$highlighter|get_class|substr:30|lcfirst}{if $lines > 10} collapsed{/if}">
+       <div>
+               <div class="codeBoxHeader">
+                       <div class="codeBoxHeadline">{@$highlighter->getTitle()}{if $filename}: {$filename}{/if}</div>
+               </div>
+               
+               <ol start="{$startLineNumber}">
+                       {assign var='lineNumber' value=$startLineNumber}
+                       {foreach from=$content item=line}
+                               {if $lineNumbers[$lineNumber]|isset}
+                                       <li id="{@$lineNumbers[$lineNumber]}"><a href="{@$__wcf->getAnchor($lineNumbers[$lineNumber])}" class="lineAnchor"></a>{@$line}</li>
+                               {else}
+                                       <li>{@$line}</li>
+                               {/if}
+                               
+                               {assign var='lineNumber' value=$lineNumber+1}
+                       {/foreach}
+               </ol>
+       </div>
+       
+       {if $lines > 10}
+               <span class="toggleButton" data-title-collapse="{lang}wcf.bbcode.button.collapse{/lang}" data-title-expand="{lang}wcf.bbcode.button.showAll{/lang}">{lang}wcf.bbcode.button.showAll{/lang}</span>
+               
+               {if !$__overlongCodeBoxSeen|isset}
+                       {assign var='__overlongCodeBoxSeen' value=true}
+                       <script data-relocate="true">
+                               require(['WoltLab/WCF/Bbcode/Collapsible'], function(BbcodeCollapsible) {
+                                       BbcodeCollapsible.observe();
+                               });
+                       </script>
+               {/if}
+       {/if}
+</div>
diff --git a/com.woltlab.wcf/templates/spoilerMetaCode.tpl b/com.woltlab.wcf/templates/spoilerMetaCode.tpl
new file mode 100644 (file)
index 0000000..74671a2
--- /dev/null
@@ -0,0 +1,34 @@
+<!-- begin:parser_nonessential -->
+<div class="spoilerBox jsSpoilerBox">
+       <header class="jsOnly">
+               <a class="button small jsSpoilerToggle"{if $buttonLabel} data-has-custom-label="true"{/if}>{if $buttonLabel}{$buttonLabel}{else}{lang}wcf.bbcode.spoiler.show{/lang}{/if}</a>
+       </header>
+       
+       <div style="display: none">
+               <!-- META_CODE_INNER_CONTENT -->
+       </div>
+</div>
+
+{if !$__wcfSpoilerBBCodeJavaScript|isset}
+       {assign var='__wcfSpoilerBBCodeJavaScript' value=true}
+       <script data-relocate="true">
+               elBySelAll('.jsSpoilerBox', null, function(spoilerBox) {
+                       spoilerBox.classList.remove('jsSpoilerBox');
+                       
+                       var toggleButton = elBySel('.jsSpoilerToggle', spoilerBox);
+                       var container = toggleButton.parentNode.nextElementSibling;
+                       
+                       toggleButton.addEventListener(WCF_CLICK_EVENT, function(event) {
+                               event.preventDefault();
+                               
+                               toggleButton.classList.toggle('active');
+                               window[(toggleButton.classList.contains('active') ? 'elShow' : 'elHide')](container);
+                               
+                               if (!elDataBool(toggleButton, 'has-custom-label')) {
+                                       toggleButton.textContent = (toggleButton.classList.contains('active')) ? '{lang}wcf.bbcode.spoiler.hide{/lang}' : '{lang}wcf.bbcode.spoiler.show{/lang}';
+                               }
+                       });
+               });
+       </script>
+{/if}
+<!-- end:parser_nonessential -->
index b92ec60e797c390bc448472c582e2675f0229966..51c1f0ea48293ced3ca9003ddb45feaa73fe1d78 100644 (file)
@@ -66,14 +66,10 @@ define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Di
                 * @protected
                 */
                _observeLoad: function() {
-                       this._editor.events.stopDetectChanges();
-                       
                        elBySelAll('pre', this._editor.$editor[0], (function(pre) {
                                pre.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
                                this._setTitle(pre);
                        }).bind(this));
-                       
-                       this._editor.events.startDetectChanges();
                },
                
                /**
@@ -114,8 +110,6 @@ define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Di
                _save: function(event) {
                        event.preventDefault();
                        
-                       this._editor.events.stopDetectChanges();
-                       
                        var id = 'redactor-code-' + this._elementId;
                        
                        ['file', 'highlighter', 'line'].forEach((function (attr) {
@@ -125,8 +119,6 @@ define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Di
                        this._setTitle(this._pre);
                        this._editor.caret.after(this._pre);
                        
-                       this._editor.events.startDetectChanges();
-                       
                        UiDialog.close(this);
                },
                
index 37e979686c01d781d48bbb1b3afe75b5dc2d3713..e7413b109b6ef562d82cd3ccc56f237b7695b7ad 100644 (file)
@@ -66,14 +66,10 @@ define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Di
                 * @protected
                 */
                _observeLoad: function() {
-                       this._editor.events.stopDetectChanges();
-                       
                        elBySelAll('blockquote', this._editor.$editor[0], (function(blockquote) {
                                blockquote.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
                                this._setTitle(blockquote);
                        }).bind(this));
-                       
-                       this._editor.events.startDetectChanges();
                },
                
                /**
@@ -114,8 +110,6 @@ define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Di
                _save: function(event) {
                        event.preventDefault();
                        
-                       this._editor.events.stopDetectChanges();
-                       
                        var id = 'redactor-quote-' + this._elementId;
                        
                        ['author', 'url'].forEach((function (attr) {
@@ -125,8 +119,6 @@ define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Di
                        this._setTitle(this._blockquote);
                        this._editor.caret.after(this._blockquote);
                        
-                       this._editor.events.startDetectChanges();
-                       
                        UiDialog.close(this);
                },
                
index 08c264863e234b9fa1b830035a711c4b695eb237..b598ab02bd4d67e7e087e3eebabaa13e850d5d2f 100644 (file)
@@ -71,14 +71,10 @@ define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Di
                 * @protected
                 */
                _observeLoad: function() {
-                       this._editor.events.stopDetectChanges();
-                       
                        elBySelAll('woltlab-spoiler', this._editor.$editor[0], (function(spoiler) {
                                spoiler.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
                                this._setTitle(spoiler);
                        }).bind(this));
-                       
-                       this._editor.events.startDetectChanges();
                },
                
                /**
@@ -119,15 +115,11 @@ define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Di
                _save: function(event) {
                        event.preventDefault();
                        
-                       this._editor.events.stopDetectChanges();
-                       
                        elData(this._spoiler, 'label', elById('redactor-spoiler-' + this._elementId + '-label').value);
                        
                        this._setTitle(this._spoiler);
                        this._editor.caret.after(this._spoiler);
                        
-                       this._editor.events.startDetectChanges();
-                       
                        UiDialog.close(this);
                },
                
diff --git a/wcfsetup/install/files/lib/system/bbcode/highlighter/BrainfuckHighlighter.class.php b/wcfsetup/install/files/lib/system/bbcode/highlighter/BrainfuckHighlighter.class.php
deleted file mode 100644 (file)
index 69d3d1b..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-namespace wcf\system\bbcode\highlighter;
-
-/**
- * Highlights syntax of brainfuck.
- * 
- * @author     Tim Duesterhus
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package    WoltLabSuite\Core\System\Bbcode\Highlighter
- */
-class BrainfuckHighlighter extends Highlighter {
-       /**
-        * @inheritDoc
-        */
-       public function highlight($string) {
-               $string = preg_replace('/[^-\\+\\.,\\[\\]\\>\\<]+/', '||span class="hlComments"||\\0||/span||', $string);
-               $string = preg_replace('/[\\<\\>]+/', '<span class="hlKeywords4">\\0</span>', $string);
-               $string = preg_replace('/[-\\+]+/', '<span class="hlKeywords1">\\0</span>', $string);
-               $string = preg_replace('/[\\.,]+/', '<span class="hlKeywords2">\\0</span>', $string);
-               $string = preg_replace('/[\\[\\]]+/', '<span class="hlKeywords3">\\0</span>', $string);
-               
-               $string = str_replace(['||span class="hlComments"||', '||/span||'], ['<span class="hlComments">', '</span>'], $string);
-               return $string;
-       }
-}
index a89bf0104b7cce1b696d87f98122fb0221700b36..4b7596e4d01bbdd3a37df3cb8d7b4597f3b3ce43 100644 (file)
@@ -31,22 +31,40 @@ class MessageHtmlInputFilter implements IHtmlInputFilter {
        protected function setAttributeDefinitions(\HTMLPurifier_Config $config) {
                // TODO: move this into own PHP classes
                $definition = $config->getHTMLDefinition(true);
-               $definition->addAttribute('blockquote', 'data-quote-title', 'Text');
-               $definition->addAttribute('blockquote', 'data-quote-url', 'URI');
                
+               // quotes
+               $definition->addAttribute('blockquote', 'data-author', 'Text');
+               $definition->addAttribute('blockquote', 'data-url', 'URI');
+               
+               // code
+               $definition->addAttribute('pre', 'data-file', 'Text');
+               $definition->addAttribute('pre', 'data-line', 'Number');
+               $definition->addAttribute('pre', 'data-highlighter', 'Text');
+               
+               // color
                $definition->addElement('woltlab-color', 'Inline', 'Inline', '', ['class' => 'Text']);
+               
+               // size
                $definition->addElement('woltlab-size', 'Inline', 'Inline', '', ['class' => 'Text']);
                
+               // mention
                $definition->addElement('woltlab-mention', 'Inline', 'Inline', '', [
                        'data-user-id' => 'Number',
                        'data-username' => 'Text'
                ]);
                
+               // spoiler
+               $definition->addElement('woltlab-spoiler', 'Block', 'Flow', '', [
+                       'data-label' => 'Text'
+               ]);
+               
+               // generic metacode
                $definition->addElement('woltlab-metacode', 'Inline', 'Inline', '', [
                        'data-attributes' => 'Text',
                        'data-name' => 'Text'
                ]);
                
+               // metacode markers
                $definition->addElement('woltlab-metacode-marker', 'Inline', 'Empty', '', [
                        'data-attributes' => 'Text',
                        'data-name' => 'Text',
diff --git a/wcfsetup/install/files/lib/system/html/metacode/converter/CodeMetacodeConverter.class.php b/wcfsetup/install/files/lib/system/html/metacode/converter/CodeMetacodeConverter.class.php
new file mode 100644 (file)
index 0000000..462253b
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+namespace wcf\system\html\metacode\converter;
+
+/**
+ * TOOD documentation
+ * @since      3.0
+ */
+class CodeMetacodeConverter extends AbstractMetacodeConverter {
+       /**
+        * @inheritDoc
+        */
+       public function convert(\DOMDocumentFragment $fragment, array $attributes) {
+               $element = $fragment->ownerDocument->createElement('pre');
+               
+               $line = 1;
+               $highlighter = $file = '';
+               
+               switch (count($attributes)) {
+                       case 1:
+                               if (is_numeric($attributes[0])) {
+                                       $line = intval($attributes[0]);
+                               }
+                               else if (mb_strpos($attributes[0], '.') === false) {
+                                       $highlighter = $attributes[0];
+                               }
+                               else {
+                                       $file = $attributes[0];
+                               }
+                               break;
+                       
+                       case 2:
+                               if (is_numeric($attributes[0])) {
+                                       $line = intval($attributes[0]);
+                                       if (mb_strpos($attributes[1], '.') === false) {
+                                               $highlighter = $attributes[1];
+                                       }
+                                       else {
+                                               $file = $attributes[1];
+                                       }
+                               }
+                               else {
+                                       $highlighter = $attributes[0];
+                                       $file = $attributes[1];
+                               }
+                               break;
+                       
+                       default:
+                               $highlighter = $attributes[0];
+                               $line = intval($attributes[1]);
+                               $file = $attributes[2];
+                               break;
+               }
+               
+               $element->setAttribute('data-file', $file);
+               $element->setAttribute('data-highlighter', $highlighter);
+               $element->setAttribute('data-line', $line);
+               
+               $element->appendChild($fragment);
+               
+               return $element;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function validateAttributes(array $attributes) {
+               // 0-3 attributes
+               return (count($attributes) <= 3);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/html/metacode/converter/SpoilerMetacodeConverter.class.php b/wcfsetup/install/files/lib/system/html/metacode/converter/SpoilerMetacodeConverter.class.php
new file mode 100644 (file)
index 0000000..89ab85c
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+namespace wcf\system\html\metacode\converter;
+use wcf\util\StringUtil;
+
+/**
+ * TOOD documentation
+ * @since      3.0
+ */
+class SpoilerMetacodeConverter extends AbstractMetacodeConverter {
+       /**
+        * @inheritDoc
+        */
+       public function convert(\DOMDocumentFragment $fragment, array $attributes) {
+               $element = $fragment->ownerDocument->createElement('woltlab-spoiler');
+               $element->setAttribute('data-label', (!empty($attributes[0])) ? StringUtil::trim($attributes[0]) : '');
+               $element->appendChild($fragment);
+               
+               return $element;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function validateAttributes(array $attributes) {
+               // 0 or 1 attribute
+               return (count($attributes) <= 1);
+       }
+}
index 261b082156602d575a03aa8caf5e223558a4515b..0c0e4f6668add3e5ca1bfd8532fe7a032c62a3b9 100644 (file)
@@ -21,34 +21,33 @@ class HtmlNodeProcessor implements IHtmlNodeProcessor {
        protected $xpath;
        
        public function load($html) {
-               $this->document = new \DOMDocument();
+               $this->document = new \DOMDocument('1.0', 'UTF-8');
                $this->xpath = null;
                
-               // convert entities as DOMDocument screws them up
-               $html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
-               
                // ignore all errors when loading the HTML string, because DOMDocument does not
                // provide a proper way to add custom HTML elements (even though explicitly allowed
                // in HTML5) and the input HTML has already been sanitized by HTMLPurifier
-               @$this->document->loadHTML($html);
+               // 
+               // we're also injecting a bogus meta tag that magically enables DOMDocument
+               // to handle UTF-8 properly, this avoids encoding non-ASCII characters as it
+               // would conflict with already existing entities when reverting them
+               @$this->document->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . $html);
                
                $this->nodeData = [];
        }
        
        public function getHtml() {
-               $html = $this->document->saveHTML();
+               $html = $this->document->saveHTML($this->document->getElementsByTagName('body')->item(0));
                
                // remove nuisance added by PHP
-               $html = preg_replace('~^<!DOCTYPE[^>]+>\s<html><body>~', '', $html);
-               $html = preg_replace('~</body></html>$~', '', $html);
-               
-               $html = mb_convert_encoding($html, 'UTF-8', 'HTML-ENTITIES');
+               $html = preg_replace('~^<body>~', '', $html);
+               $html = preg_replace('~</body>$~', '', $html);
                
                /** @var IHtmlNode $obj */
                foreach ($this->nodeData as $data) {
                        $obj = $data['object'];
                        $string = $obj->replaceTag($data['data']);
-                       $html = preg_replace_callback('~<wcfNode-' . $data['identifier'] . '>(?P<content>.*)</wcfNode-' . $data['identifier'] . '>~', function($matches) use ($string) {
+                       $html = preg_replace_callback('~<wcfNode-' . $data['identifier'] . '>(?P<content>[\s\S]*)</wcfNode-' . $data['identifier'] . '>~', function($matches) use ($string) {
                                $string = str_replace('<!-- META_CODE_INNER_CONTENT -->', $matches['content'], $string);
                                
                                return $string;
index 611f66bca9a7df25288e70b9d3c3d047c0c37a98..7aed0e40dc1a94547b3f797fa12d6f0c29c86401 100644 (file)
@@ -18,19 +18,15 @@ class HtmlOutputNodeBlockquote extends AbstractHtmlNode {
         * @inheritDoc
         */
        public function process(array $elements, HtmlNodeProcessor $htmlNodeProcessor) {
+               /** @var \DOMElement $element */
                foreach ($elements as $element) {
-                       if ($element->getAttribute('class') === 'quoteBox') {
-                               $nodeIdentifier = StringUtil::getRandomID();
-                               $htmlNodeProcessor->addNodeData($this, $nodeIdentifier, [
-                                       'title' => ($element->hasAttribute('data-quote-title')) ? $element->getAttribute('data-quote-title') : '',
-                                       'url' => ($element->hasAttribute('data-quote-url')) ? $element->getAttribute('data-quote-url') : ''
-                               ]);
-                               
-                               $htmlNodeProcessor->renameTag($element, 'wcfNode-' . $nodeIdentifier);
-                       }
-                       else {
-                               $htmlNodeProcessor->unwrapContent($element);
-                       }
+                       $nodeIdentifier = StringUtil::getRandomID();
+                       $htmlNodeProcessor->addNodeData($this, $nodeIdentifier, [
+                               'title' => ($element->hasAttribute('data-quote-title')) ? $element->getAttribute('data-quote-title') : '',
+                               'url' => ($element->hasAttribute('data-quote-url')) ? $element->getAttribute('data-quote-url') : ''
+                       ]);
+                       
+                       $htmlNodeProcessor->renameTag($element, 'wcfNode-' . $nodeIdentifier);
                }
        }
        
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
new file mode 100644 (file)
index 0000000..25f7454
--- /dev/null
@@ -0,0 +1,219 @@
+<?php
+namespace wcf\system\html\output\node;
+use wcf\system\bbcode\highlighter\BashHighlighter;
+use wcf\system\bbcode\highlighter\CHighlighter;
+use wcf\system\bbcode\highlighter\DiffHighlighter;
+use wcf\system\bbcode\highlighter\HtmlHighlighter;
+use wcf\system\bbcode\highlighter\JavaHighlighter;
+use wcf\system\bbcode\highlighter\JsHighlighter;
+use wcf\system\bbcode\highlighter\PerlHighlighter;
+use wcf\system\bbcode\highlighter\PhpHighlighter;
+use wcf\system\bbcode\highlighter\PlainHighlighter;
+use wcf\system\bbcode\highlighter\PythonHighlighter;
+use wcf\system\bbcode\highlighter\SqlHighlighter;
+use wcf\system\bbcode\highlighter\TexHighlighter;
+use wcf\system\bbcode\highlighter\XmlHighlighter;
+use wcf\system\html\node\AbstractHtmlNode;
+use wcf\system\html\node\HtmlNodeProcessor;
+use wcf\system\Regex;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * TOOD documentation
+ * @since      3.0
+ */
+class HtmlOutputNodePre extends AbstractHtmlNode {
+       protected $tagName = 'pre';
+       
+       /**
+        * already used ids for line numbers to prevent duplicate ids in the output
+        * @var string[]
+        */
+       private static $codeIDs = [];
+       
+       /**
+        * @inheritDoc
+        */
+       public function process(array $elements, HtmlNodeProcessor $htmlNodeProcessor) {
+               /** @var \DOMElement $element */
+               foreach ($elements as $element) {
+                       $nodeIdentifier = StringUtil::getRandomID();
+                       $htmlNodeProcessor->addNodeData($this, $nodeIdentifier, [
+                               'content' => $element->textContent,
+                               'file' => ($element->hasAttribute('data-file')) ? $element->getAttribute('data-file') : '',
+                               'highlighter' => ($element->hasAttribute('data-highlighter')) ? $element->getAttribute('data-highlighter') : '',
+                               'line' => ($element->hasAttribute('data-line')) ? $element->getAttribute('data-line') : 1
+                       ]);
+                       
+                       $htmlNodeProcessor->renameTag($element, 'wcfNode-' . $nodeIdentifier);
+               }
+       }
+       
+       public function replaceTag(array $data) {
+               $content = preg_replace('/^\s*\n/', '', $data['content']);
+               $content = preg_replace('/\n\s*$/', '', $content);
+               
+               $file = $data['file'];
+               $highlighter = $data['highlighter'];
+               $line = ($data['line'] < 1) ? 1 : $data['line'];
+               
+               // fetch highlighter-classname
+               $className = PlainHighlighter::class;
+               
+               // no highlighting for strings over a certain size, to prevent DoS
+               // this serves as a safety net in case one of the regular expressions
+               // in a highlighter causes PCRE to exhaust resources, such as the stack
+               if (strlen($content) < 16384) {
+                       if ($highlighter) {
+                               $className = '\wcf\system\bbcode\highlighter\\'.StringUtil::firstCharToUpperCase(mb_strtolower($highlighter)).'Highlighter';
+                               
+                               switch (mb_substr($className, strlen('\wcf\system\bbcode\highlighter\\'))) {
+                                       case 'ShellHighlighter':
+                                               $className = BashHighlighter::class;
+                                               break;
+                                       
+                                       case 'C++Highlighter':
+                                               $className = CHighlighter::class;
+                                               break;
+                                       
+                                       case 'JavascriptHighlighter':
+                                               $className = JsHighlighter::class;
+                                               break;
+                                       
+                                       case 'LatexHighlighter':
+                                               $className = TexHighlighter::class;
+                                               break;
+                               }
+                       }
+                       else {
+                               // try to guess highlighter
+                               if (mb_strpos($content, '<?php') !== false) {
+                                       $className = PhpHighlighter::class;
+                               }
+                               else if (mb_strpos($content, '<html') !== false) {
+                                       $className = HtmlHighlighter::class;
+                               }
+                               else if (mb_strpos($content, '<?xml') === 0) {
+                                       $className = XmlHighlighter::class;
+                               }
+                               else if (       mb_strpos($content, 'SELECT') === 0
+                                       ||      mb_strpos($content, 'UPDATE') === 0
+                                       ||      mb_strpos($content, 'INSERT') === 0
+                                       ||      mb_strpos($content, 'DELETE') === 0) {
+                                       $className = SqlHighlighter::class;
+                               }
+                               else if (mb_strpos($content, 'import java.') !== false) {
+                                       $className = JavaHighlighter::class;
+                               }
+                               else if (       mb_strpos($content, "---") !== false
+                                       &&      mb_strpos($content, "\n+++") !== false) {
+                                       $className = DiffHighlighter::class;
+                               }
+                               else if (mb_strpos($content, "\n#include ") !== false) {
+                                       $className = CHighlighter::class;
+                               }
+                               else if (mb_strpos($content, '#!/usr/bin/perl') === 0) {
+                                       $className = PerlHighlighter::class;
+                               }
+                               else if (mb_strpos($content, 'def __init__(self') !== false) {
+                                       $className = PythonHighlighter::class;
+                               }
+                               else if (Regex::compile('^#!/bin/(ba|z)?sh')->match($content)) {
+                                       $className = BashHighlighter::class;
+                               }
+                               else if (mb_strpos($content, '\\documentclass') !== false) {
+                                       $className = TexHighlighter::class;
+                               }
+                       }
+               }
+               
+               if (!class_exists($className)) {
+                       $className = PlainHighlighter::class;
+               }
+               
+               /** @noinspection PhpUndefinedMethodInspection */
+               $highlightedContent = $this->fixMarkup(explode("\n", $className::getInstance()->highlight($content)));
+               
+               // show template
+               /** @noinspection PhpUndefinedMethodInspection */
+               WCF::getTPL()->assign([
+                       'lineNumbers' => $this->makeLineNumbers($content, $line),
+                       'startLineNumber' => $line,
+                       'content' => $highlightedContent,
+                       'highlighter' => $className::getInstance(),
+                       'filename' => $file,
+                       'lines' => substr_count($content, "\n") + 1
+               ]);
+               
+               return WCF::getTPL()->fetch('codeMetaCode');
+       }
+       
+       /**
+        * Returns a string with all line numbers
+        *
+        * @param       string          $code
+        * @param       integer         $start
+        * @param       string          $split
+        * @return      string
+        */
+       protected function makeLineNumbers($code, $start, $split = "\n") {
+               $lines = explode($split, $code);
+               
+               $lineNumbers = [];
+               $i = -1;
+               // find an unused codeID
+               do {
+                       $codeID = mb_substr(StringUtil::getHash($code), 0, 6).(++$i ? '_'.$i : '');
+               }
+               while (isset(self::$codeIDs[$codeID]));
+               
+               // mark codeID as used
+               self::$codeIDs[$codeID] = true;
+               
+               for ($i = $start, $j = count($lines) + $start; $i < $j; $i++) {
+                       $lineNumbers[$i] = 'codeLine_'.$i.'_'.$codeID;
+               }
+               return $lineNumbers;
+       }
+       
+       /**
+        * Fixes markup that every line has proper number of opening and closing tags
+        *
+        * @param       string[]        $lines
+        * @return      string[]
+        */
+       protected function fixMarkup(array $lines) {
+               static $spanRegex = null;
+               static $emptyTagRegex = null;
+               if ($spanRegex === null) {
+                       $spanRegex = new Regex('(?:<span(?: class="(?:[^"])*")?>|</span>)');
+                       $emptyTagRegex = new Regex('<span(?: class="(?:[^"])*")?></span>');
+               }
+               
+               $openTags = [];
+               foreach ($lines as &$line) {
+                       $spanRegex->match($line, true);
+                       // open all tags again
+                       $line = implode('', $openTags).$line;
+                       $matches = $spanRegex->getMatches();
+                       
+                       // parse opening and closing spans
+                       foreach ($matches[0] as $match) {
+                               if ($match === '</span>') array_pop($openTags);
+                               else {
+                                       array_push($openTags, $match);
+                               }
+                       }
+                       
+                       // close all tags
+                       $line .= str_repeat('</span>', count($openTags));
+                       
+                       // remove empty tags to avoid cluttering the output
+                       $line = $emptyTagRegex->replace($line, '');
+               }
+               unset($line);
+               
+               return $lines;
+       }
+}
index 26139e9123b1e6934e1d68b5091854797b33d822..8648ffdac52509ba61841a3de0ffc536d342354a 100644 (file)
@@ -15,5 +15,7 @@ class HtmlOutputNodeProcessor extends HtmlNodeProcessor {
                $this->invokeHtmlNode(new HtmlOutputNodeWoltlabMention());
                $this->invokeHtmlNode(new HtmlOutputNodeWoltlabColor());
                $this->invokeHtmlNode(new HtmlOutputNodeWoltlabSize());
+               $this->invokeHtmlNode(new HtmlOutputNodeWoltlabSpoiler());
+               $this->invokeHtmlNode(new HtmlOutputNodePre());
        }
 }
diff --git a/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeWoltlabSpoiler.class.php b/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeWoltlabSpoiler.class.php
new file mode 100644 (file)
index 0000000..921099d
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+namespace wcf\system\html\output\node;
+use wcf\system\application\ApplicationHandler;
+use wcf\system\html\node\AbstractHtmlNode;
+use wcf\system\html\node\HtmlNodeProcessor;
+use wcf\system\request\RouteHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * TOOD documentation
+ * @since      3.0
+ */
+class HtmlOutputNodeWoltlabSpoiler extends AbstractHtmlNode {
+       protected $tagName = 'woltlab-spoiler';
+       
+       /**
+        * @inheritDoc
+        */
+       public function process(array $elements, HtmlNodeProcessor $htmlNodeProcessor) {
+               /** @var \DOMElement $element */
+               foreach ($elements as $element) {
+                       $nodeIdentifier = StringUtil::getRandomID();
+                       $htmlNodeProcessor->addNodeData($this, $nodeIdentifier, [
+                               'label' => ($element->hasAttribute('data-label')) ? $element->getAttribute('data-label') : ''
+                       ]);
+                       
+                       $htmlNodeProcessor->renameTag($element, 'wcfNode-' . $nodeIdentifier);
+               }
+       }
+       
+       public function replaceTag(array $data) {
+               WCF::getTPL()->assign([
+                       'buttonLabel' => $data['label']
+               ]);
+               return WCF::getTPL()->fetch('spoilerMetaCode');
+       }
+}
index 8672a96a4f2393e8fad78f062b4137b5e0390832..1660df226cd07d60eeb2c31751d6ecce106ac1c1 100644 (file)
                @include wcfFontHeadline;
        }
 }
+
+.codeBox {
+       background-color: $wcfContentBackground;
+       box-shadow: 0 0 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24);
+       border-radius: 2px;
+       margin-top: 1em;
+       padding: 10px;
+       
+       > div {
+               > ol {
+                       margin-left: 3.4em !important;
+                       position: relative;
+                       
+                       &::before {
+                               border-right: 1px solid $wcfContentBorderInner;
+                               bottom: 0;
+                               content: "";
+                               position: absolute;
+                               top: 0;
+                       }
+                       
+                       > li {
+                               padding-left: 5px;
+                       }
+               }
+       }
+}
+
+/* ############## Code Styles ############## */
+
+/* -- -- -- Code Box -- -- -- */
+
+.codeBox .hlQuotes {
+       color: red;
+}
+
+.codeBox .hlComments,
+.codeBox .hlOperators {
+       color: green;
+}
+
+.codeBox .hlKeywords1 {
+       color: blue;
+}
+
+.codeBox .hlKeywords2 {
+       color: darkred;
+}
+
+.codeBox .hlKeywords3 {
+       color: darkviolet;
+}
+
+.codeBox .hlKeywords4 {
+       color: darkgoldenrod;
+}
+
+.codeBox .hlKeywords5 {
+       color: crimson;
+}
+
+.codeBox .hlNumbers {
+       color: darkorange;
+}
+
+/* -- -- -- Code Highlighters -- -- -- */
+
+/* DIFF */
+
+.diffHighlighter .hlComments {
+       color: darkviolet;
+}
+
+.diffHighlighter .hlRemoved {
+       color: red;
+}
+
+.diffHighlighter .hlAdded {
+       color: green;
+}
+
+/* PHP */
+
+.phpHighlighter .hlKeywords2 {
+       color: green;
+}
+
+.phpHighlighter .hlComments {
+       color: darkgoldenrod;
+}
+
+/* CSS */
+
+.cssHighlighter .hlComments {
+       color: #236e26;
+}
+
+.cssHighlighter .hlColors {
+       color: #751116;
+}
+
+.cssHighlighter .hlNumbers,
+.sqlHighlighter .hlNumbers {
+       color: #1906fd;
+}
+
+.cssHighlighter .hlKeywords1 {
+       color: #87154f;
+}
+
+.cssHighlighter .hlKeywords2 {
+       color: #994509;
+}
+
+.cssHighlighter .hlKeywords3,
+.cssHighlighter .hlKeywords4 {
+       color: inherit;
+}
+
+/* SQL */
+
+.sqlHighlighter .hlKeywords1 {
+       color: #663821;
+}
+
+.sqlHighlighter .hlKeywords2 {
+       color: #871550;
+}
index 4c2477432135b13e50f4432f4cebbd6af79e3c59..fb95574c9cdebeaf672e962fbc558e4ebfdf999c 100644 (file)
@@ -1777,7 +1777,6 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getFormattedAllowedExt
        <category name="wcf.bbcode">
                <item name="wcf.bbcode.code"><![CDATA[Code]]></item>
                <item name="wcf.bbcode.code.bash.title"><![CDATA[Shell-Script]]></item>
-               <item name="wcf.bbcode.code.brainfuck.title"><![CDATA[Brainfuck]]></item>
                <item name="wcf.bbcode.code.c.title"><![CDATA[C]]></item>
                <item name="wcf.bbcode.code.css.title"><![CDATA[CSS]]></item>
                <item name="wcf.bbcode.code.diff.title"><![CDATA[Diff]]></item>
index 17d585e483b97c826999605c146c2bedf05cd4a4..c4ad3104af1513576ba4eef47e4138da7d4ee788 100644 (file)
@@ -1784,7 +1784,6 @@ Allowed extensions: {', '|implode:$attachmentHandler->getFormattedAllowedExtensi
        <category name="wcf.bbcode">
                <item name="wcf.bbcode.code"><![CDATA[Code]]></item>
                <item name="wcf.bbcode.code.bash.title"><![CDATA[Shell-Script]]></item>
-               <item name="wcf.bbcode.code.brainfuck.title"><![CDATA[Brainfuck]]></item>
                <item name="wcf.bbcode.code.c.title"><![CDATA[C]]></item>
                <item name="wcf.bbcode.code.css.title"><![CDATA[CSS]]></item>
                <item name="wcf.bbcode.code.diff.title"><![CDATA[Diff]]></item>