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
* @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 });
* @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)
--- /dev/null
+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;
+});
--- /dev/null
+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;
+});
--- /dev/null
+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, '&');
+ 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] = '</' + 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;
+});