First draft
authorAlexander Ebert <ebert@woltlab.com>
Sat, 27 Jun 2015 18:20:44 +0000 (20:20 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 27 Jun 2015 18:20:44 +0000 (20:20 +0200)
Does not work out of the box right now, it uses a modified 'wysiwyg' template to provide the AMD modules.

Features:
- works in linebreak mode
- uses a bbcode parser similar to the PHP implementation (XML-conformity)
- HTML to BBCode uses native DOM functions

wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js
wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js [new file with mode: 0644]

index ee446b238a00e54477e70dd6c718790818675c7e..d009526bf7b91a57060f48c75ed1671806c4502a 100644 (file)
@@ -290,6 +290,9 @@ RedactorPlugins.wbbcode = function() {
                 * @param       string          html
                 */
                convertFromHtml: function(html) {
+                       // DEBUG ONLY
+                       return this.opts.woltlab.bbcode.fromHtml.convert(html);
+                       
                        var $searchFor = [ ];
                        
                        WCF.System.Event.fireEvent('com.woltlab.wcf.redactor', 'beforeConvertFromHtml', { html: html });
@@ -845,6 +848,9 @@ RedactorPlugins.wbbcode = function() {
                 * @param       string          data
                 */
                convertToHtml: function(data) {
+                       // DEBUG ONLY
+                       return this.opts.woltlab.bbcode.toHtml.convert(data);
+                       
                        WCF.System.Event.fireEvent('com.woltlab.wcf.redactor', 'beforeConvertToHtml', { data: data });
                        
                        // remove 0x200B (unicode zero width space)
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js
new file mode 100644 (file)
index 0000000..b4db14d
--- /dev/null
@@ -0,0 +1,172 @@
+define([], function() {
+       "use strict";
+       
+       var _converter = [];
+       var _inlineConverter = [];
+       
+       var BBCodeFromHtml = {
+               convert: function(message) {
+                       if (message.length) this._setup();
+                       
+                       var container = document.createElement('div');
+                       container.innerHTML = message;
+                       
+                       // convert line breaks
+                       var elements = container.getElementsByTagName('P');
+                       while (elements.length) elements[0].outerHTML = elements[0].innerHTML;
+                       
+                       var elements = container.getElementsByTagName('BR');
+                       while (elements.length) elements[0].outerHTML = "\n";
+                       
+                       for (var i = 0, length = _converter.length; i < length; i++) {
+                               this._convert(container, _converter[i]);
+                       }
+                       
+                       message = this._convertSpecials(container.innerHTML);
+                       
+                       return message;
+               },
+               
+               _convertSpecials: function(message) {
+                       message = message.replace(/&amp;/g, '&');
+                       message = message.replace(/&lt;/g, '<');
+                       message = message.replace(/&gt;/g, '>');
+                       
+                       return message;
+               },
+               
+               _setup: function() {
+                       if (_converter.length) {
+                               return;
+                       }
+                       
+                       _converter = [
+                               // simple replacement
+                               { tagName: 'STRONG', bbcode: 'b' },
+                               { tagName: 'DEL', bbcode: 's' },
+                               { tagName: 'EM', bbcode: 'i' },
+                               { tagName: 'SUB', bbcode: 'sub' },
+                               { tagName: 'SUP', bbcode: 'sup' },
+                               { tagName: 'U', bbcode: 'u' },
+                               
+                               // callback replacement
+                               { tagName: 'A', callback: this._convertUrl.bind(this) },
+                               { tagName: 'LI', callback: this._convertListItem.bind(this) },
+                               { tagName: 'OL', callback: this._convertList.bind(this) },
+                               { tagName: 'UL', callback: this._convertList.bind(this) },
+                               { tagName: 'SPAN', callback: this._convertSpan.bind(this) }
+                       ];
+                       
+                       _inlineConverter = [
+                               { style: 'color', callback: this._convertInlineColor.bind(this) },
+                               { style: 'font-size', callback: this._convertInlineFontSize.bind(this) },
+                               { style: 'font-family', callback: this._convertInlineFontFamily.bind(this) }
+                       ];
+               },
+               
+               _convert: function(container, converter) {
+                       if (typeof converter === 'function') {
+                               converter(container);
+                               return;
+                       }
+                       
+                       var element, elements = container.getElementsByTagName(converter.tagName);
+                       while (elements.length) {
+                               element = elements[0];
+                               
+                               if (converter.bbcode) {
+                                       element.outerHTML = '[' + converter.bbcode + ']' + element.innerHTML + '[/' + converter.bbcode + ']';
+                               }
+                               else {
+                                       converter.callback(element);
+                               }
+                       }
+               },
+               
+               _convertList: function(element) {
+                       var open;
+                       
+                       if (element.nodeName === 'OL') {
+                               open = '[list=1]';
+                       }
+                       else {
+                               var type = element.style.getPropertyValue('list-style-type') || '';
+                               if (type === '') {
+                                       open = '[list]';
+                               }
+                               else {
+                                       open = '[list=' + (type === 'lower-latin' ? 'a' : type) + ']';
+                               }
+                       }
+                       
+                       element.outerHTML = open + element.innerHTML + '[/list]';
+               },
+               
+               _convertListItem: function(element) {
+                       if (element.parentNode.nodeName !== 'UL' && element.parentNode.nodeName !== 'OL') {
+                               element.outerHTML = element.innerHTML;
+                       }
+                       else {
+                               element.outerHTML = '[*]' + element.innerHTML;
+                       }
+               },
+               
+               _convertSpan: function(element) {
+                       if (element.style.length || element.className) {
+                               var converter, value;
+                               for (var i = 0, length = _inlineConverters.length; i < length; i++) {
+                                       converter = _inlineConverters[i];
+                                       
+                                       if (converter.style) {
+                                               value = element.style.getPropertyValue(converter.style) || '';
+                                               if (value) {
+                                                       converter.callback(element, value);
+                                               }
+                                       }
+                                       else {
+                                               if (element.classList.contains(converter.className)) {
+                                                       converter.callback(element);
+                                               }
+                                       }
+                               }
+                       }
+                       
+                       element.outerHTML = element.innerHTML;
+               },
+               
+               _convertInlineColor: function(element, color) {
+                       if (color.match(/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/i)) {
+                               var r = RegExp.$1;
+                               var g = RegExp.$2;
+                               var b = RegExp.$3;
+                               
+                               var chars = '0123456789ABCDEF';
+                               color = '#' + (chars.charAt((r - r % 16) / 16) + '' + chars.charAt(r % 16)) + '' + (chars.charAt((g - g % 16) / 16) + '' + chars.charAt(g % 16)) + '' + (chars.charAt((b - b % 16) / 16) + '' + chars.charAt(b % 16));
+                       }
+                       
+                       element.innerHTML = '[color=' + color + ']' + element.innerHTML + '[/color]';
+               },
+               
+               _convertUrl: function(element) {
+                       var content = element.textContent.trim(), href = element.href.trim();
+                       
+                       if (href === '' || content === '') {
+                               // empty href or content
+                               element.outerHTML = element.innerHTML;
+                               return;
+                       }
+                       
+                       if (href.indexOf('mailto:') === 0) {
+                               element.outerHTML = '[email=' + href.substr(6) + ']' + element.innerHTML + '[/email]';
+                       }
+                       else if (href === content) {
+                               element.outerHTML = '[url]' + href + '[/url]';
+                       }
+                       else {
+                               element.outerHTML = "[url='" + href + "']" + element.innerHTML + "[/url]";
+                       }
+               }
+       };
+       
+       return BBCodeFromHtml;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js
new file mode 100644 (file)
index 0000000..eb69565
--- /dev/null
@@ -0,0 +1,153 @@
+define([], function() {
+       "use strict";
+       
+       var BBCodeParser = {
+               parse: function(message) {
+                       var stack = this._splitTags(message);
+                       this._buildLinearTree(stack);
+                       
+                       return stack;
+               },
+               
+               _splitTags: function(message) {
+                       var validTags = 'attach|b|color|i|list|url';
+                       var pattern = '(\\\[(?:/(?:' + validTags + ')|(?:' + validTags + ')'
+                               + '(?:='
+                                       + '(?:\\\'[^\\\'\\\\]*(?:\\\\.[^\\\'\\\\]*)*\\\'|[^,\\\]]*)'
+                                       + '(?:,(?:\\\'[^\\\'\\\\]*(?:\\\\.[^\\\'\\\\]*)*\'|[^,\\\]]*))*'
+                               + ')?)\\\])';
+                       
+                       var isBBCode = new RegExp('^' + pattern + '$', 'i');
+                       var part, parts = message.split(new RegExp(pattern, 'i')), stack = [], tag;
+                       for (var i = 0, length = parts.length; i < length; i++) {
+                               part = parts[i];
+                               
+                               if (part === '') {
+                                       continue;
+                               }
+                               else if (part.match(isBBCode)) {
+                                       tag = { name: '', closing: false, source: part };
+                                       
+                                       if (part[1] === '/') {
+                                               tag.name = part.substring(2, part.length - 1);
+                                               tag.closing = true;
+                                       }
+                                       else if (part.match(/^\[([a-z0-9]+)=?(.*)\]$/i)) {
+                                               tag.name = RegExp.$1;
+                                               
+                                               if (RegExp.$2) {
+                                                       tag.attributes = this._parseAttributes(RegExp.$2);
+                                               }
+                                       }
+                                       
+                                       stack.push(tag);
+                               }
+                               else {
+                                       stack.push(part);
+                               }
+                       }
+                       
+                       return stack;
+               },
+               
+               _buildLinearTree: function(stack) {
+                       var item, openTags = [], reopenTags, sourceBBCode = '', tag;
+                       for (var i = 0, length = stack.length; i < length; i++) {
+                               item = stack[i];
+                               
+                               if (typeof item === 'object') {
+                                       if (sourceBBCode.length && (item.name !== sourceBBCode || item.closing === false)) {
+                                               stack[i] = item.source;
+                                       }
+                                       
+                                       if (item.closing) {
+                                               var lastIndex = this._findOpenTag(openTags, item.name);
+                                               if (lastIndex === -1) {
+                                                       // tag was never opened, treat as plain text
+                                                       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) {
+                                                       sourceBBCode = '';
+                                               }
+                                       }
+                                       else {  
+                                               openTags.push(item);
+                                               
+                                               if (__REDACTOR_SOURCE_BBCODES.indexOf(item.name) !== -1) {
+                                                       sourceBBCode = item.name;
+                                               }
+                                       }
+                               }
+                       }
+                       
+                       // close unclosed tags
+                       this._closeUnclosedTags(stack, openTags, '');
+               },
+               
+               _closeUnclosedTags: function(stack, openTags, until) {
+                       var item, reopenTags = [], tag;
+                       
+                       for (var i = openTags.length - 1; i >= 0; i--) {
+                               item = openTags[i];
+                               
+                               if (item.name === until) {
+                                       break;
+                               }
+                               
+                               tag = { name: item.name, closing: true, source: '[/' + item.name + ']' };
+                               item.pair = stack.length;
+                               
+                               stack.push(tag);
+                               
+                               openTags.pop();
+                               reopenTags.push({ name: item.name, closing: false, source: item.source });
+                       }
+                       
+                       return reopenTags.reverse();
+               },
+               
+               _findOpenTag: function(openTags, name) {
+                       for (var i = openTags.length - 1; i >= 0; i--) {
+                               if (openTags[i].name === name) {
+                                       return i;
+                               }
+                       }
+                       
+                       return -1;
+               },
+               
+               _parseAttributes: function(attrString) {
+                       var tmp = attrString.match(/(?:^|,)('[^'\\\\]*(?:\\\\.[^'\\\\]*)*'|[^,]*)/g);
+                       
+                       var attribute, attributes = [];
+                       for (var i = 0, length = tmp.length; i < length; i++) {
+                               attribute = tmp[i];
+                               
+                               if (attribute !== '') {
+                                       if (attribute[0] === "'" && attribute.substr(-1) === "'") {
+                                               attributes.push(attribute.substring(1, attribute.length - 1));
+                                       }
+                                       else {
+                                               attributes.push(attribute);
+                                       }
+                               }
+                       }
+                       
+                       return attributes;
+               }
+       }
+       
+       return BBCodeParser;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js
new file mode 100644 (file)
index 0000000..7027f46
--- /dev/null
@@ -0,0 +1,131 @@
+define(['WoltLab/WCF/BBCode/Parser'], function(BBCodeParser) {
+       "use strict";
+       
+       var _bbcodes = null;
+       
+       var BBCodeToHtml = {
+               convert: function(message) {
+                       this._convertSpecials(message);
+                       
+                       var stack = BBCodeParser.parse(message);
+                       
+                       if (stack.length) {
+                               this._initBBCodes();
+                       }
+                       
+                       var item;
+                       for (var i = 0, length = stack.length; i < length; i++) {
+                               item = stack[i];
+                               
+                               if (typeof item === 'object') {
+                                       stack[i] = this._replace(stack, item, i);
+                               }
+                       }
+                       
+                       message = stack.join('');
+                       
+                       message = message.replace(/\n/g, '<br>');
+                       
+                       return message;
+               },
+               
+               _convertSpecials: function(message) {
+                       message = message.replace(/&/g, '&amp;');
+                       message = message.replace(/</g, '&lt;');
+                       message = message.replace(/>/g, '&gt;');
+                       
+                       return message;
+               },
+               
+               _initBBCodes: function() {
+                       if (_bbcodes !== null) {
+                               return;
+                       }
+                       
+                       _bbcodes = {
+                               // simple replacements
+                               b: 'strong',
+                               i: 'em',
+                               u: 'u',
+                               s: 'del',
+                               sub: 'sub',
+                               sup: 'sup',
+                               
+                               // callback replacement
+                               color: this._replaceColor.bind(this),
+                               list: this._replaceList.bind(this),
+                               url: this._replaceUrl.bind(this)
+                       };
+               },
+               
+               _replace: function(stack, item, index) {
+                       var pair = stack[item.pair], replace = _bbcodes[item.name];
+                       
+                       if (replace === undefined) {
+                               // treat as plain text
+                               stack[item.pair] = pair.source;
+                               
+                               return item.source;
+                       }
+                       else if (typeof replace === 'string') {
+                               stack[item.pair] = '</' + replace + '>';
+                               
+                               return '<' + replace + '>';
+                       }
+                       else {
+                               return replace(stack, item, pair, index);
+                       }
+               },
+               
+               _replaceColor: function(stack, item, pair) {
+                       if (item.attributes === undefined || !item.attributes.length || !item.attributes[0].match(/^[a-z0-9#]+$/i)) {
+                               stack[item.pair] = '';
+                               
+                               return '';
+                       }
+                       
+                       stack[item.pair] = '</span>';
+                       
+                       return '<span style="color: ' + item.attributes[0] + '">';
+               },
+               
+               _replaceList: function(stack, item, pair, index) {
+                       var type = (item.attributes === undefined || !items.attributes.length) ? '' : item.attributes[0].trim();
+                       
+                       // replace list items
+                       for (var i = index + 1; i < item.pair; i++) {
+                               if (typeof stack[i] === 'string') {
+                                       stack[i] = stack[i].replace(/\[\*\]/g, '<li>');
+                               }
+                       }
+                       
+                       if (type == '1' || type === 'decimal') {
+                               stack[item.pair] = '</ol>';
+                               
+                               return '<ol>';
+                       }
+                       
+                       stack[item.pair] = '</ul>';
+                       if (type.length && type.match(/^(?:none|circle|square|disc|decimal|lower-roman|upper-roman|decimal-leading-zero|lower-greek|lower-latin|upper-latin|armenian|georgian)$/)) {
+                               return '<ul style="list-style-type: ' + type + '">';
+                       }
+                       
+                       return '<ul>';
+               },
+               
+               _replaceUrl: function(stack, item, pair) {
+                       // ignore url bbcode without arguments
+                       if (item.attributes === undefined || !item.attributes.length) {
+                               stack[item.pair] = '';
+                               
+                               return '';
+                       }
+                       
+                       stack[item.pair] = '</a>';
+                       
+                       return '<a href="' + item.attributes[0] + '">';
+               }
+       };
+       
+       return BBCodeToHtml;
+});