From 93d79307806616e7d7dda9bf9dbbf45435ab6aed Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 27 Jun 2015 20:20:44 +0200 Subject: [PATCH] First draft 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 --- .../js/3rdParty/redactor/plugins/wbbcode.js | 6 + .../files/js/WoltLab/WCF/BBCode/FromHtml.js | 172 ++++++++++++++++++ .../files/js/WoltLab/WCF/BBCode/Parser.js | 153 ++++++++++++++++ .../files/js/WoltLab/WCF/BBCode/ToHtml.js | 131 +++++++++++++ 4 files changed, 462 insertions(+) create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js diff --git a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js index ee446b238a..d009526bf7 100644 --- a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js +++ b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js @@ -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 index 0000000000..b4db14d541 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js @@ -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(/&/g, '&'); + message = message.replace(/</g, '<'); + message = message.replace(/>/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 index 0000000000..eb69565bc3 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js @@ -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 index 0000000000..7027f468da --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js @@ -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, '
'); + + return message; + }, + + _convertSpecials: function(message) { + message = message.replace(/&/g, '&'); + message = message.replace(//g, '>'); + + 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] = ''; + + 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] = ''; + + return ''; + }, + + _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, '
  • '); + } + } + + if (type == '1' || type === 'decimal') { + stack[item.pair] = ''; + + return '
      '; + } + + stack[item.pair] = ''; + 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 '
        '; + } + + return '
          '; + }, + + _replaceUrl: function(stack, item, pair) { + // ignore url bbcode without arguments + if (item.attributes === undefined || !item.attributes.length) { + stack[item.pair] = ''; + + return ''; + } + + stack[item.pair] = ''; + + return ''; + } + }; + + return BBCodeToHtml; +}); -- 2.20.1