Added support for source bbcodes and improved parser
authorAlexander Ebert <ebert@woltlab.com>
Tue, 30 Jun 2015 13:44:30 +0000 (15:44 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 30 Jun 2015 13:44:30 +0000 (15:44 +0200)
wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js
wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js
wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js

index ed992aca32a521dce6098b3108af7339d7aee567..0e3cafdffadae3aa9aeeb9c9d4c898c26c91337f 100644 (file)
@@ -1,8 +1,9 @@
-define(['StringUtil', 'DOM/Traverse'], function(StringUtil, DOMTraverse) {
+define(['EventHandler', 'StringUtil', 'DOM/Traverse'], function(EventHandler, StringUtil, DOMTraverse) {
        "use strict";
        
        var _converter = [];
        var _inlineConverter = {};
+       var _sourceConverter = [];
        
        var BBCodeFromHtml = {
                convert: function(message) {
@@ -18,15 +19,58 @@ define(['StringUtil', 'DOM/Traverse'], function(StringUtil, DOMTraverse) {
                        var elements = container.getElementsByTagName('BR');
                        while (elements.length) elements[0].outerHTML = "\n";
                        
+                       var sourceElements = this._preserveSourceElements(container);
+                       
                        for (var i = 0, length = _converter.length; i < length; i++) {
                                this._convert(container, _converter[i]);
                        }
                        
+                       this._restoreSourceElements(container, sourceElements);
+                       
                        message = this._convertSpecials(container.innerHTML);
                        
                        return message;
                },
                
+               _preserveSourceElements: function(container) {
+                       var elements, sourceElements = [], tmp;
+                       
+                       for (var i = 0, length = _sourceConverter.length; i < length; i++) {
+                               elements = container.querySelectorAll(_sourceConverter[i].selector);
+                               
+                               tmp = [];
+                               for (var j = 0, innerLength = elements.length; j < innerLength; j++) {
+                                       this._preserveSourceElement(elements[j], tmp);
+                               }
+                               
+                               sourceElements.push(tmp);
+                       }
+                       
+                       return sourceElements;
+               },
+               
+               _restoreSourceElements: function(container, sourceElements) {
+                       var element, elements, placeholder;
+                       for (var i = 0, length = sourceElements.length; i < length; i++) {
+                               elements = sourceElements[i];
+                               
+                               if (elements.length === 0) {
+                                       continue;
+                               }
+                               
+                               for (var j = 0, innerLength = elements.length; j < innerLength; j++) {
+                                       element = elements[j];
+                                       placeholder = element.placeholder;
+                                       
+                                       placeholder.parentNode.insertBefore(element.fragment, placeholder);
+                                       
+                                       _sourceConverter[i].callback(placeholder.previousElementSibling);
+                                       
+                                       placeholder.parentNode.removeChild(placeholder);
+                               }
+                       }
+               },
+               
                _convertSpecials: function(message) {
                        message = message.replace(/&amp;/g, '&');
                        message = message.replace(/&lt;/g, '<');
@@ -72,6 +116,16 @@ define(['StringUtil', 'DOM/Traverse'], function(StringUtil, DOMTraverse) {
                                        { style: 'text-align', callback: this._convertInlineTextAlign.bind(this) }
                                ]
                        };
+                       
+                       _sourceConverter = [
+                               { selector: 'div.codeBox', callback: this._convertSourceCodeBox.bind(this) }
+                       ];
+                       
+                       EventHandler.fire('com.woltlab.wcf.bbcode.fromHtml', 'init', {
+                               converter: _converter,
+                               inlineConverter: _inlineConverter,
+                               sourceConverter: _sourceConverter
+                       });
                },
                
                _convert: function(container, converter) {
@@ -171,14 +225,19 @@ define(['StringUtil', 'DOM/Traverse'], function(StringUtil, DOMTraverse) {
                },
                
                _convertDiv: function(element) {
-                       if (element.style.length) {
+                       if (element.className.length || element.style.length) {
                                var converter, value;
                                for (var i = 0, length = _inlineConverter.div.length; i < length; i++) {
                                        converter = _inlineConverter.div[i];
                                        
-                                       value = element.style.getPropertyValue(converter.style) || '';
-                                       if (value) {
-                                               converter.callback(element, value);
+                                       if (converter.className && element.classList.contains(converter.className)) {
+                                               converter.callback(element);
+                                       }
+                                       else if (converter.style) {
+                                               value = element.style.getPropertyValue(converter.style) || '';
+                                               if (value) {
+                                                       converter.callback(element, value);
+                                               }
                                        }
                                }
                        }
@@ -264,6 +323,37 @@ define(['StringUtil', 'DOM/Traverse'], function(StringUtil, DOMTraverse) {
                        else {
                                element.outerHTML = "[url='" + href + "']" + element.innerHTML + "[/url]";
                        }
+               },
+               
+               _convertSourceCodeBox: function(element) {
+                       var filename = element.getAttribute('data-filename').trim() || '';
+                       var highlighter = element.getAttribute('data-highlighter') || '';
+                       window.dtdesign = element;
+                       var list = DOMTraverse.childByTag(element.children[0], 'OL');
+                       var lineNumber = ~~list.getAttribute('start') || 1;
+                       
+                       var content = '';
+                       for (var i = 0, length = list.childElementCount; i < length; i++) {
+                               if (content) content += "\n";
+                               content += list.children[i].textContent;
+                       }
+                       
+                       var open = "[code='" + highlighter + "'," + lineNumber + ",'" + filename + "']";
+                       
+                       element.outerHTML = open + content + '[/code]';
+               },
+               
+               _preserveSourceElement: function(element, sourceElements) {
+                       var placeholder = document.createElement('var');
+                       element.parentNode.insertBefore(placeholder, element);
+                       
+                       var fragment = document.createDocumentFragment();
+                       fragment.appendChild(element);
+                       
+                       sourceElements.push({
+                               fragment: fragment,
+                               placeholder: placeholder
+                       });
                }
        };
        
index 13bc0708890591addd5a8fb87dccacb24c3d661c..9e72dc9d54756662c209394d63c7bc7dc02ad725 100644 (file)
@@ -11,7 +11,7 @@ define([], function() {
                
                _splitTags: function(message) {
                        // TODO: `validTags` should be dynamic similar to the PHP implementation
-                       var validTags = 'attach|b|color|i|list|url|table|td|tr|quote';
+                       var validTags = 'attach|b|code|color|i|list|url|table|td|tr|quote';
                        var pattern = '(\\\[(?:/(?:' + validTags + ')|(?:' + validTags + ')'
                                + '(?:='
                                        + '(?:\\\'[^\\\'\\\\]*(?:\\\\.[^\\\'\\\\]*)*\\\'|[^,\\\]]*)'
@@ -53,12 +53,13 @@ define([], function() {
                
                _buildLinearTree: function(stack) {
                        var item, openTags = [], reopenTags, sourceBBCode = '', tag;
-                       for (var i = 0, length = stack.length; i < length; i++) {
+                       for (var i = 0; i < stack.length; i++) { // do not cache stack.length, its size is dynamic
                                item = stack[i];
                                
                                if (typeof item === 'object') {
-                                       if (sourceBBCode.length && (item.name !== sourceBBCode || item.closing === false)) {
+                                       if (sourceBBCode.length && (item.name !== sourceBBCode || !item.closing)) {
                                                stack[i] = item.source;
+                                               continue;
                                        }
                                        
                                        if (item.closing) {
@@ -68,14 +69,26 @@ define([], function() {
                                                        stack[i] = item.source;
                                                }
                                                else {
-                                                       reopenTags = this._closeUnclosedTags(stack, openTags, item.name);
-                                                       
                                                        tag = openTags.pop();
                                                        tag.pair = i;
                                                        
-                                                       for (var j = 0, innerLength = reopenTags.length; j < innerLength; j++) {
-                                                               stack.splice(i, reopenTags[j]);
-                                                               i++;
+                                                       if (sourceBBCode === item.name) {
+                                                               // join previous items in the stack
+                                                               if (lastIndex + 2 < i) {
+                                                                       var joinWith = lastIndex + 1;
+                                                                       for (var j = lastIndex + 2; j < i; j++) {
+                                                                               stack[joinWith] += stack[j];
+                                                                               stack[j] = '';
+                                                                       }
+                                                               }
+                                                       }
+                                                       else {
+                                                               reopenTags = this._closeUnclosedTags(stack, openTags, item.name);
+                                                               
+                                                               for (var j = 0, innerLength = reopenTags.length; j < innerLength; j++) {
+                                                                       stack.splice(i, reopenTags[j]);
+                                                                       i++;
+                                                               }
                                                        }
                                                }
                                                
index 151927c128948d7c37a0c69c877718dc9e0783ab..25166aede1e1fe70331465c235f98eb0083f3ff0 100644 (file)
@@ -1,10 +1,14 @@
-define(['Language', 'StringUtil', 'WoltLab/WCF/BBCode/Parser'], function(Language, StringUtil, BBCodeParser) {
+define(['EventHandler', 'Language', 'StringUtil', 'WoltLab/WCF/BBCode/Parser'], function(EventHandler, Language, StringUtil, BBCodeParser) {
        "use strict";
        
        var _bbcodes = null;
        var _removeNewlineAfter = [];
        var _removeNewlineBefore = [];
        
+       function isNumber(value) { return value && value == ~~value; }
+       function isFilename(value) { return (value.indexOf('.') !== -1) || (!isNumber(value) && !isHighlighter(value)); }
+       function isHighlighter(value) { return __REDACTOR_CODE_HIGHLIGHTERS.hasOwnProperty(value); }
+       
        var BBCodeToHtml = {
                convert: function(message) {
                        this._convertSpecials(message);
@@ -25,7 +29,8 @@ define(['Language', 'StringUtil', 'WoltLab/WCF/BBCode/Parser'], function(Languag
                        }
                        
                        message = stack.join('');
-                       
+                       var x = message;
+                       console.debug(x);
                        message = message.replace(/\n/g, '<br>');
                        
                        return message;
@@ -58,6 +63,7 @@ define(['Language', 'StringUtil', 'WoltLab/WCF/BBCode/Parser'], function(Languag
                                
                                // callback replacement
                                color: this._replaceColor.bind(this),
+                               code: this._replaceCode.bind(this),
                                list: this._replaceList.bind(this),
                                quote: this._replaceQuote.bind(this),
                                url: this._replaceUrl.bind(this)
@@ -65,6 +71,12 @@ define(['Language', 'StringUtil', 'WoltLab/WCF/BBCode/Parser'], function(Languag
                        
                        _removeNewlineAfter = ['quote', 'table', 'td', 'tr'];
                        _removeNewlineBefore = ['table', 'td', 'tr'];
+                       
+                       EventHandler.fire('com.woltlab.wcf.bbcode.toHtml', 'init', {
+                               bbcodes: _bbcodes,
+                               removeNewlineAfter: _removeNewlineAfter,
+                               removeNewlineBefore: _removeNewlineBefore
+                       });
                },
                
                _replace: function(stack, item, index) {
@@ -115,6 +127,88 @@ define(['Language', 'StringUtil', 'WoltLab/WCF/BBCode/Parser'], function(Languag
                        }
                },
                
+               _replaceCode: function(stack, item, index) {
+                       var attributes = item.attributes, filename = '', highlighter = 'auto', lineNumber = 0;
+                       
+                       // parse arguments
+                       switch (attributes.length) {
+                               case 1:
+                                       if (isNumber(attributes[0])) {
+                                               lineNumber = ~~attributes[0];
+                                       }
+                                       else if (isFilename(attributes[0])) {
+                                               filename = attributes[0];
+                                       }
+                                       else if (isHighlighter(attributes[0])) {
+                                               highlighter = attributes[0];
+                                       }
+                                       break;
+                               case 2:
+                                       if (isNumber(attributes[0])) {
+                                               lineNumber = ~~attributes[0];
+                                               
+                                               if (isHighlighter(attributes[1])) {
+                                                       highlighter = attributes[1];
+                                               }
+                                               else if (isFilename(attributes[1])) {
+                                                       filename = attributes[1];
+                                               }
+                                       }
+                                       else {
+                                               if (isHighlighter(attributes[0])) highlighter = attributes[0];
+                                               if (isFilename(attributes[1])) filename = attributes[1];
+                                       }
+                                       break;
+                               case 3:
+                                       if (isHighlighter(attributes[0])) highlighter = attributes[0];
+                                       if (isNumber(attributes[1])) lineNumber = ~~attributes[1];
+                                       if (isFilename(attributes[2])) filename = attributes[2];
+                                       break;
+                       }
+                       
+                       // transform content
+                       var before = true, content, line, empty = -1;
+                       for (var i = index + 1; i < item.pair; i++) {
+                               line = stack[i];
+                               
+                               if (line.trim() === '') {
+                                       if (before) {
+                                               stack[i] = '';
+                                               continue;
+                                       }
+                                       else if (empty === -1) {
+                                               empty = i;
+                                       }
+                               }
+                               else {
+                                       before = false;
+                                       empty = -1;
+                               }
+                               
+                               content = line.split('\n');
+                               for (var j = 0, innerLength = content.length; j < innerLength; j++) {
+                                       content[j] = '<li>' + (content[j] ? StringUtil.escapeHTML(content[j]) : '\u200b') + '</li>';
+                               }
+                               
+                               stack[i] = content.join('');
+                       }
+                       
+                       if (!before && empty !== -1) {
+                               for (var i = item.pair - 1; i >= empty; i--) {
+                                       stack[i] = '';
+                               }
+                       }
+                       
+                       stack[item.pair] = '</ol></div></div>';
+                       
+                       return '<div class="codeBox container" contenteditable="false" data-highlighter="' + highlighter + '" data-filename="' + (filename ? StringUtil.escapeHTML(filename) : '') + '">'
+                                       + '<div>'
+                                       + '<div>'
+                                               + '<h3>' + __REDACTOR_CODE_HIGHLIGHTERS[highlighter] + (filename ? ': ' + StringUtil.escapeHTML(filename) : '') + '</h3>'
+                                       + '</div>'
+                                       + '<ol start="' + (lineNumber > 1 ? lineNumber : 1) + '">';
+               },
+               
                _replaceColor: function(stack, item, index) {
                        if (!item.attributes.length || !item.attributes[0].match(/^[a-z0-9#]+$/i)) {
                                stack[item.pair] = '';