+/**
+ * Converts a message containing HTML tags into BBCodes.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2015 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLab/WCF/BBCode/FromHtml
+ */
define(['EventHandler', 'StringUtil', 'DOM/Traverse'], function(EventHandler, StringUtil, DOMTraverse) {
"use strict";
var _inlineConverter = {};
var _sourceConverter = [];
+ /**
+ * Returns true if a whitespace should be inserted before or after the smiley.
+ *
+ * @param {Element} element image element
+ * @param {boolean} before evaluate previous node
+ * @return {boolean} true if a whitespace should be inserted
+ */
function addSmileyPadding(element, before) {
var target = element[(before ? 'previousSibling' : 'nextSibling')];
if (target === null || target.nodeType !== Node.TEXT_NODE || !/\s$/.test(target.textContent)) {
return false;
}
+ /**
+ * @module WoltLab/WCF/BBCode/FromHtml
+ */
var BBCodeFromHtml = {
+ /**
+ * Converts a message containing HTML elements into BBCodes.
+ *
+ * @param {string} message message containing HTML elements
+ * @return {string} message containing BBCodes
+ */
convert: function(message) {
if (message.length) this._setup();
var elements = container.getElementsByTagName('P');
while (elements.length) elements[0].outerHTML = elements[0].innerHTML;
- var elements = container.getElementsByTagName('BR');
+ elements = container.getElementsByTagName('BR');
while (elements.length) elements[0].outerHTML = "\n";
+ // prevent conversion taking place inside source bbcodes
var sourceElements = this._preserveSourceElements(container);
+ EventHandler.fire('com.woltlab.wcf.bbcode.fromHtml', 'beforeConvert', { container: container });
+
for (var i = 0, length = _converter.length; i < length; i++) {
this._convert(container, _converter[i]);
}
+ EventHandler.fire('com.woltlab.wcf.bbcode.fromHtml', 'afterConvert', { container: container });
+
this._restoreSourceElements(container, sourceElements);
+ // remove remaining HTML elements
+ elements = container.getElementsByTagName('*');
+ while (elements.length) elements[0].outerHTML = elements[0].innerHTML;
+
message = this._convertSpecials(container.innerHTML);
return message;
},
+ /**
+ * Replaces HTML elements mapping to source BBCodes to avoid
+ * them being handled by other converters.
+ *
+ * @param {Element} container container element
+ * @return {array<object>} list of source elements and their placeholder
+ */
_preserveSourceElements: function(container) {
var elements, sourceElements = [], tmp;
return sourceElements;
},
+ /**
+ * Replaces an element with a placeholder.
+ *
+ * @param {Element} element target element
+ * @param {array<object>} list of removed elements and their placeholders
+ */
+ _preserveSourceElement: function(element, sourceElements) {
+ var placeholder = document.createElement('var');
+ placeholder.setAttribute('data-source', 'wcf');
+ element.parentNode.insertBefore(placeholder, element);
+
+ var fragment = document.createDocumentFragment();
+ fragment.appendChild(element);
+
+ sourceElements.push({
+ fragment: fragment,
+ placeholder: placeholder
+ });
+ },
+
+ /**
+ * Reinserts source elements for parsing.
+ *
+ * @param {Element} container container element
+ * @param {array<object>} sourceElements list of removed elements and their placeholders
+ */
_restoreSourceElements: function(container, sourceElements) {
var element, elements, placeholder;
for (var i = 0, length = sourceElements.length; i < length; i++) {
}
},
+ /**
+ * Converts special entities.
+ *
+ * @param {string} message HTML message
+ * @return {string} HTML message
+ */
_convertSpecials: function(message) {
message = message.replace(/&/g, '&');
message = message.replace(/</g, '<');
return message;
},
+ /**
+ * Sets up converters applied to elements in linear order.
+ */
_setup: function() {
if (_converter.length) {
return;
});
},
+ /**
+ * Converts an element into a raw string.
+ *
+ * @param {Element} container container element
+ * @param {object} converter converter object
+ */
_convert: function(container, converter) {
if (typeof converter === 'function') {
converter(container);
}
},
+ /**
+ * Converts <blockquote> into [quote].
+ *
+ * @param {Element} element target element
+ */
_convertBlockquote: function(element) {
var author = element.getAttribute('data-author') || '';
var link = element.getAttribute('cite') || '';
element.outerHTML = open + element.innerHTML.replace(/^\n*/, '').replace(/\n*$/, '') + '[/quote]\n';
},
+ /**
+ * Converts <img> into smilies, [attach] or [img].
+ *
+ * @param {Element} element target element
+ */
_convertImage: function(element) {
if (element.classList.contains('smiley')) {
// smiley
}
},
+ /**
+ * Converts <ol> and <ul> into [list].
+ *
+ * @param {Element} element target element
+ */
_convertList: function(element) {
var open;
element.outerHTML = open + element.innerHTML + '[/list]';
},
+ /**
+ * Converts <li> into [*] unless it is not encapsulated in <ol> or <ul>.
+ *
+ * @param {Element} element target element
+ */
_convertListItem: function(element) {
if (element.parentNode.nodeName !== 'UL' && element.parentNode.nodeName !== 'OL') {
element.outerHTML = element.innerHTML;
}
},
+ /**
+ * Converts <span> into a series of BBCodes including [color], [font] and [size].
+ *
+ * @param {Element} element target element
+ */
_convertSpan: function(element) {
if (element.style.length || element.className) {
var converter, value;
element.outerHTML = element.innerHTML;
},
+ /**
+ * Converts <div> into a series of BBCodes including [align].
+ *
+ * @param {Element} element target element
+ */
_convertDiv: function(element) {
if (element.className.length || element.style.length) {
var converter, value;
element.outerHTML = element.innerHTML;
},
+ /**
+ * Converts the CSS style `color` into [color].
+ *
+ * @param {Element} element target element
+ */
_convertInlineColor: function(element, value) {
if (value.match(/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/i)) {
var r = RegExp.$1;
element.innerHTML = '[color=' + value + ']' + element.innerHTML + '[/color]';
},
+ /**
+ * Converts the CSS style `font-size` into [size].
+ *
+ * @param {Element} element target element
+ */
_convertInlineFontSize: function(element, value) {
if (value.match(/^(\d+)pt$/)) {
value = RegExp.$1;
}
},
+ /**
+ * Converts the CSS style `font-family` into [font].
+ *
+ * @param {Element} element target element
+ */
_convertInlineFontFamily: function(element, value) {
element.innerHTML = '[font=' + value.replace(/'/g, '') + ']' + element.innerHTML + '[/font]';
},
+ /**
+ * Converts the CSS style `text-align` into [align].
+ *
+ * @param {Element} element target element
+ */
_convertInlineTextAlign: function(element, value) {
if (value === 'left' || value === 'right' || value === 'justify') {
element.innerHTML = '[align=' + value + ']' + innerHTML + '[/align]';
}
},
+ /**
+ * Converts tables and their children into BBCodes.
+ *
+ * @param {Element} element target element
+ */
_convertTable: function(element) {
var elements = element.getElementsByTagName('TD');
while (elements.length) {
element.outerHTML = '\n[table]' + innerHtml + '\n[/table]\n';
},
+ /**
+ * Converts <a> into [email] or [url].
+ *
+ * @param {Element} element target element
+ */
_convertUrl: function(element) {
var content = element.textContent.trim(), href = element.href.trim(), tagName = 'url';
}
},
+ /**
+ * Converts <div class="codeBox"> into [code].
+ *
+ * @param {Element} element target element
+ */
_convertSourceCodeBox: function(element) {
var filename = element.getAttribute('data-filename').trim() || '';
var highlighter = element.getAttribute('data-highlighter') || '';
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
- });
}
};
+/**
+ * Versatile BBCode parser based upon the PHP implementation.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2015 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLab/WCF/BBCode/Parser
+ */
define([], function() {
"use strict";
+ /**
+ * @module WoltLab/WCF/BBCode/Parser
+ */
var BBCodeParser = {
+ /**
+ * Parses a message and returns an XML-conform linear tree.
+ *
+ * @param {string} message message containing BBCodes
+ * @return {array<mixed>} linear tree
+ */
parse: function(message) {
var stack = this._splitTags(message);
this._buildLinearTree(stack);
return stack;
},
+ /**
+ * Splits message into strings and BBCode objects.
+ *
+ * @param {string} message message containing BBCodes
+ * @returns {array<mixed>} linear tree
+ */
_splitTags: function(message) {
- // TODO: `validTags` should be dynamic similar to the PHP implementation
var validTags = __REDACTOR_BBCODES.join('|');
var pattern = '(\\\[(?:/(?:' + validTags + ')|(?:' + validTags + ')'
+ '(?:='
return stack;
},
+ /**
+ * Finds pairs and enforces XML-conformity in terms of pairing and proper nesting.
+ *
+ * @param {array<mixed>} stack linear tree
+ */
_buildLinearTree: function(stack) {
- var item, openTags = [], reopenTags, sourceBBCode = '', tag;
+ var item, openTags = [], reopenTags, sourceBBCode = '';
for (var i = 0; i < stack.length; i++) { // do not cache stack.length, its size is dynamic
item = stack[i];
this._closeUnclosedTags(stack, openTags, '');
},
+ /**
+ * Closes unclosed BBCodes and returns a list of BBCodes in order of appearance that should be
+ * opened again to enforce proper nesting.
+ *
+ * @param {array<mixed>} stack linear tree
+ * @param {array<object>} openTags list of unclosed elements
+ * @param {string} until tag name to stop at
+ * @return {array<mixed>} list of tags to open in order of appearance
+ */
_closeUnclosedTags: function(stack, openTags, until) {
var item, reopenTags = [], tag;
return reopenTags.reverse();
},
+ /**
+ * Returns true if given BBCode was opened before.
+ *
+ * @param {array<object>} openTags list of unclosed elements
+ * @param {string} name BBCode to search for
+ * @returns {boolean} false if tag was not opened before
+ */
_hasOpenTag: function(openTags, name) {
for (var i = openTags.length - 1; i >= 0; i--) {
if (openTags[i].name === name) {
return false;
},
+ /**
+ * Parses the attribute list and returns a list of attributes without enclosing quotes.
+ *
+ * @param {string} attrString comma separated string with optional quotes per attribute
+ * @returns {array<string>} list of attributes
+ */
_parseAttributes: function(attrString) {
var tmp = attrString.split(/(?:^|,)('[^'\\\\]*(?:\\\\.[^'\\\\]*)*'|[^,]*)/g);
+/**
+ * Converts a message containing BBCodes into HTML.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2015 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLab/WCF/BBCode/ToHtml
+ */
define(['Core', 'EventHandler', 'Language', 'StringUtil', 'WoltLab/WCF/BBCode/Parser'], function(Core, EventHandler, Language, StringUtil, BBCodeParser) {
"use strict";
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); }
+ /**
+ * Returns true if given value is a non-zero integer.
+ *
+ * @param {string} value target value
+ * @return {boolean} true if `value` is a non-zero integer
+ */
+ function isNumber(value) {
+ return value && value == ~~value;
+ }
+ /**
+ * Returns true if given value appears to be a filename, which means that it contains a dot
+ * or is neither numeric nor a known highlighter.
+ *
+ * @param {string} value target value
+ * @return {boolean} true if `value` appears to be a filename
+ */
+ function isFilename(value) {
+ return (value.indexOf('.') !== -1) || (!isNumber(value) && !isHighlighter(value));
+ }
+
+ /**
+ * Returns true if given value is a known highlighter.
+ *
+ * @param {string} value target value
+ * @return {boolean} true if `value` is a known highlighter
+ */
+ function isHighlighter(value) {
+ return __REDACTOR_CODE_HIGHLIGHTERS.hasOwnProperty(value);
+ }
+
+ /**
+ * @module WoltLab/WCF/BBCode/ToHtml
+ */
var BBCodeToHtml = {
+ /**
+ * Converts a message containing BBCodes to HTML.
+ *
+ * @param {string} message message containing BBCodes
+ * @return {string} HTML message
+ */
convert: function(message, options) {
_options = Core.extend({
attachments: {
this._initBBCodes();
}
+ EventHandler.fire('com.woltlab.wcf.bbcode.toHtml', 'beforeConvert', { stack: stack });
+
var item, value;
for (var i = 0, length = stack.length; i < length; i++) {
item = stack[i];
}
}
+ EventHandler.fire('com.woltlab.wcf.bbcode.toHtml', 'afterConvert', { stack: stack });
+
message = stack.join('');
message = message.replace(/\n/g, '<br>');
return message;
},
+ /**
+ * Converts special characters to their entities.
+ *
+ * @param {string} message message containing BBCodes
+ * @return {string} message with replaced special characters
+ */
_convertSpecials: function(message) {
message = message.replace(/&/g, '&');
message = message.replace(/</g, '<');
return message;
},
+ /**
+ * Sets up converters applied to HTML elements.
+ */
_initBBCodes: function() {
if (_bbcodes !== null) {
return;
});
},
+ /**
+ * Converts an item from the stack.
+ *
+ * @param {array<mixed>} stack linear list of BBCode tags and regular strings
+ * @param {object} item current BBCode tag object
+ * @param {integer} index current stack index representing `item`
+ * @return {(string|array)} string if only the current item should be replaced or an array with
+ * the first item used for the opening tag and the second item for the closing tag
+ */
_convert: function(stack, item, index) {
var replace = _bbcodes[item.name], tmp;
}
},
+ /**
+ * Converts [attach] into an <img> or to plain text if attachment is a non-image.
+ *
+ * @param {array<mixed>} stack linear list of BBCode tags and regular strings
+ * @param {object} item current BBCode tag object
+ * @param {integer} index current stack index representing `item`
+ * @returns {array} first item represents the opening tag, the second the closing one
+ */
_convertAttachment: function(stack, item, index) {
var attachmentId = 0, attributes = item.attributes, length = attributes.length;
if (!_options.attachments.url) {
];
},
+ /**
+ * Converts [code] to <div class="codeBox">.
+ *
+ * @param {array<mixed>} stack linear list of BBCode tags and regular strings
+ * @param {object} item current BBCode tag object
+ * @param {integer} index current stack index representing `item`
+ * @returns {array} first item represents the opening tag, the second the closing one
+ */
_convertCode: function(stack, item, index) {
var attributes = item.attributes, filename = '', highlighter = 'auto', lineNumber = 0;
];
},
+ /**
+ * Converts [color] to <span style="color: ...">.
+ *
+ * @param {array<mixed>} stack linear list of BBCode tags and regular strings
+ * @param {object} item current BBCode tag object
+ * @param {integer} index current stack index representing `item`
+ * @returns {array} first item represents the opening tag, the second the closing one
+ */
_convertColor: function(stack, item, index) {
if (!item.attributes.length || !item.attributes[0].match(/^[a-z0-9#]+$/i)) {
return [null, null];
return ['<span style="color: ' + StringUtil.escapeHTML(item.attributes[0]) + '">', '</span>'];
},
+ /**
+ * Converts [email] to <a href="mailto: ...">.
+ *
+ * @param {array<mixed>} stack linear list of BBCode tags and regular strings
+ * @param {object} item current BBCode tag object
+ * @param {integer} index current stack index representing `item`
+ * @returns {array} first item represents the opening tag, the second the closing one
+ */
_convertEmail: function(stack, item, index) {
var email = '';
if (item.attributes.length) {
return ['<a href="mailto:' + StringUtil.escapeHTML(email) + '">', '</a>'];
},
+ /**
+ * Converts [img] to <img>.
+ *
+ * @param {array<mixed>} stack linear list of BBCode tags and regular strings
+ * @param {object} item current BBCode tag object
+ * @param {integer} index current stack index representing `item`
+ * @returns {array} first item represents the opening tag, the second the closing one
+ */
_convertImage: function(stack, item, index) {
var float = 'none', source = '', width = 0;
return ['<img src="' + StringUtil.escapeHTML(source) + '"' + (styles.length ? ' style="' + styles.join(';') + '"' : '') + '>', ''];
},
+ /**
+ * Converts [list] to <ol> or <ul>.
+ *
+ * @param {array<mixed>} stack linear list of BBCode tags and regular strings
+ * @param {object} item current BBCode tag object
+ * @param {integer} index current stack index representing `item`
+ * @returns {array} first item represents the opening tag, the second the closing one
+ */
_convertList: function(stack, item, index) {
var type = (items.attributes.length) ? item.attributes[0] : '';
return ['<ul>', '</ul>'];
},
+ /**
+ * Converts [quote] to <blockquote>.
+ *
+ * @param {array<mixed>} stack linear list of BBCode tags and regular strings
+ * @param {object} item current BBCode tag object
+ * @param {integer} index current stack index representing `item`
+ * @returns {array} first item represents the opening tag, the second the closing one
+ */
_convertQuote: function(stack, item, index) {
var author = '', link = '';
if (item.attributes.length > 1) {
];
},
+ /**
+ * Converts smiley codes into <img>.
+ *
+ * @param {array<mixed>} stack linear list of BBCode tags and regular strings
+ */
_convertSmilies: function(stack) {
var altValue, item, regexp;
for (var i = 0, length = stack.length; i < length; i++) {
}
},
+ /**
+ * Converts [size] to <span style="font-size: ...">.
+ *
+ * @param {array<mixed>} stack linear list of BBCode tags and regular strings
+ * @param {object} item current BBCode tag object
+ * @param {integer} index current stack index representing `item`
+ * @returns {array} first item represents the opening tag, the second the closing one
+ */
_convertSize: function(stack, item, index) {
if (!item.attributes.length || ~~item.attributes[0] === 0) {
return [null, null];
return ['<span style="font-size: ' + ~~item.attributes[0] + 'pt">', '</span>'];
},
+ /**
+ * Converts [url] to <a>.
+ *
+ * @param {array<mixed>} stack linear list of BBCode tags and regular strings
+ * @param {object} item current BBCode tag object
+ * @param {integer} index current stack index representing `item`
+ * @returns {array} first item represents the opening tag, the second the closing one
+ */
_convertUrl: function(stack, item, index) {
// ignore url bbcode without arguments
if (!item.attributes.length) {