Mercurial Hosting > sceditor
view src/formats/bbcode.js @ 8:292d40f68d50
minor
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Fri, 05 Aug 2022 12:56:50 -0600 |
parents | b7725dab7482 |
children | 0cb206904499 |
line wrap: on
line source
/** * SCEditor BBCode Plugin * http://www.sceditor.com/ * * Copyright (C) 2011-2017, Sam Clarke (samclarke.com) * * SCEditor is licensed under the MIT license: * http://www.opensource.org/licenses/mit-license.php * * @fileoverview SCEditor BBCode Format * @author Sam Clarke */ (function (sceditor) { /*eslint max-depth: off*/ 'use strict'; var escapeEntities = sceditor.escapeEntities; var escapeUriScheme = sceditor.escapeUriScheme; var dom = sceditor.dom; var utils = sceditor.utils; var css = dom.css; var attr = dom.attr; var is = dom.is; var extend = utils.extend; var each = utils.each; var EMOTICON_DATA_ATTR = 'data-sceditor-emoticon'; var getEditorCommand = sceditor.command.get; var QuoteType = { /** @lends BBCodeParser.QuoteType */ /** * Always quote the attribute value * @type {Number} */ always: 1, /** * Never quote the attributes value * @type {Number} */ never: 2, /** * Only quote the attributes value when it contains spaces to equals * @type {Number} */ auto: 3 }; var defaultCommandsOverrides = { bold: { txtExec: ['[b]', '[/b]'] }, italic: { txtExec: ['[i]', '[/i]'] }, underline: { txtExec: ['[u]', '[/u]'] }, strike: { txtExec: ['[s]', '[/s]'] }, subscript: { txtExec: ['[sub]', '[/sub]'] }, superscript: { txtExec: ['[sup]', '[/sup]'] }, left: { txtExec: ['[left]', '[/left]'] }, center: { txtExec: ['[center]', '[/center]'] }, right: { txtExec: ['[right]', '[/right]'] }, justify: { txtExec: ['[justify]', '[/justify]'] }, font: { txtExec: function (caller) { var editor = this; getEditorCommand('font')._dropDown( editor, caller, function (fontName) { editor.insertText( '[font=' + fontName + ']', '[/font]' ); } ); } }, size: { txtExec: function (caller) { var editor = this; getEditorCommand('size')._dropDown( editor, caller, function (fontSize) { editor.insertText( '[size=' + fontSize + ']', '[/size]' ); } ); } }, color: { txtExec: function (caller) { var editor = this; getEditorCommand('color')._dropDown( editor, caller, function (color) { editor.insertText( '[color=' + color + ']', '[/color]' ); } ); } }, bulletlist: { txtExec: function (caller, selected) { this.insertText( '[ul]\n[li]' + selected.split(/\r?\n/).join('[/li]\n[li]') + '[/li]\n[/ul]' ); } }, orderedlist: { txtExec: function (caller, selected) { this.insertText( '[ol]\n[li]' + selected.split(/\r?\n/).join('[/li]\n[li]') + '[/li]\n[/ol]' ); } }, table: { txtExec: ['[table][tr][td]', '[/td][/tr][/table]'] }, horizontalrule: { txtExec: ['[hr]'] }, code: { txtExec: ['[code]', '[/code]'] }, image: { txtExec: function (caller, selected) { var editor = this; getEditorCommand('image')._dropDown( editor, caller, selected, function (url, width, height) { var attrs = ''; if (width) { attrs += ' width=' + width; } if (height) { attrs += ' height=' + height; } editor.insertText( '[img' + attrs + ']' + url + '[/img]' ); } ); } }, email: { txtExec: function (caller, selected) { var editor = this; getEditorCommand('email')._dropDown( editor, caller, function (url, text) { editor.insertText( '[email=' + url + ']' + (text || selected || url) + '[/email]' ); } ); } }, link: { txtExec: function (caller, selected) { var editor = this; getEditorCommand('link')._dropDown( editor, caller, function (url, text) { editor.insertText( '[url=' + url + ']' + (text || selected || url) + '[/url]' ); } ); } }, quote: { txtExec: ['[quote]', '[/quote]'] }, youtube: { txtExec: function (caller) { var editor = this; getEditorCommand('youtube')._dropDown( editor, caller, function (id) { editor.insertText('[youtube]' + id + '[/youtube]'); } ); } }, rtl: { txtExec: ['[rtl]', '[/rtl]'] }, ltr: { txtExec: ['[ltr]', '[/ltr]'] } }; var bbcodeHandlers = { // START_COMMAND: Bold b: { tags: { b: null, strong: null }, styles: { // 401 is for FF 3.5 'font-weight': ['bold', 'bolder', '401', '700', '800', '900'] }, format: '[b]{0}[/b]', html: '<strong>{0}</strong>' }, // END_COMMAND // START_COMMAND: Italic i: { tags: { i: null, em: null }, styles: { 'font-style': ['italic', 'oblique'] }, format: '[i]{0}[/i]', html: '<em>{0}</em>' }, // END_COMMAND // START_COMMAND: Underline u: { tags: { u: null }, styles: { 'text-decoration': ['underline'] }, format: '[u]{0}[/u]', html: '<u>{0}</u>' }, // END_COMMAND // START_COMMAND: Strikethrough s: { tags: { s: null, strike: null }, styles: { 'text-decoration': ['line-through'] }, format: '[s]{0}[/s]', html: '<s>{0}</s>' }, // END_COMMAND // START_COMMAND: Subscript sub: { tags: { sub: null }, format: '[sub]{0}[/sub]', html: '<sub>{0}</sub>' }, // END_COMMAND // START_COMMAND: Superscript sup: { tags: { sup: null }, format: '[sup]{0}[/sup]', html: '<sup>{0}</sup>' }, // END_COMMAND // START_COMMAND: Font font: { tags: { font: { face: null } }, styles: { 'font-family': null }, quoteType: QuoteType.never, format: function (element, content) { var font; if (!is(element, 'font') || !(font = attr(element, 'face'))) { font = css(element, 'font-family'); } return '[font=' + _stripQuotes(font) + ']' + content + '[/font]'; }, html: '<font face="{defaultattr}">{0}</font>' }, // END_COMMAND // START_COMMAND: Size size: { tags: { font: { size: null } }, styles: { 'font-size': null }, format: function (element, content) { var fontSize = attr(element, 'size'), size = 2; if (!fontSize) { fontSize = css(element, 'fontSize'); } // Most browsers return px value but IE returns 1-7 if (fontSize.indexOf('px') > -1) { // convert size to an int fontSize = fontSize.replace('px', '') - 0; if (fontSize < 12) { size = 1; } if (fontSize > 15) { size = 3; } if (fontSize > 17) { size = 4; } if (fontSize > 23) { size = 5; } if (fontSize > 31) { size = 6; } if (fontSize > 47) { size = 7; } } else { size = fontSize; } return '[size=' + size + ']' + content + '[/size]'; }, html: '<font size="{defaultattr}">{!0}</font>' }, // END_COMMAND // START_COMMAND: Color color: { tags: { font: { color: null } }, styles: { color: null }, quoteType: QuoteType.never, format: function (elm, content) { var color; if (!is(elm, 'font') || !(color = attr(elm, 'color'))) { color = elm.style.color || css(elm, 'color'); } return '[color=' + _normaliseColour(color) + ']' + content + '[/color]'; }, html: function (token, attrs, content) { return '<font color="' + escapeEntities(_normaliseColour(attrs.defaultattr), true) + '">' + content + '</font>'; } }, // END_COMMAND // START_COMMAND: Lists ul: { tags: { ul: null }, breakStart: true, isInline: false, skipLastLineBreak: true, format: '[ul]{0}[/ul]', html: '<ul>{0}</ul>' }, list: { breakStart: true, isInline: false, skipLastLineBreak: true, html: '<ul>{0}</ul>' }, ol: { tags: { ol: null }, breakStart: true, isInline: false, skipLastLineBreak: true, format: '[ol]{0}[/ol]', html: '<ol>{0}</ol>' }, li: { tags: { li: null }, isInline: false, closedBy: ['/ul', '/ol', '/list', '*', 'li'], format: '[li]{0}[/li]', html: '<li>{0}</li>' }, '*': { isInline: false, closedBy: ['/ul', '/ol', '/list', '*', 'li'], html: '<li>{0}</li>' }, // END_COMMAND // START_COMMAND: Table table: { tags: { table: null }, isInline: false, isHtmlInline: true, skipLastLineBreak: true, format: '[table]{0}[/table]', html: '<table>{0}</table>' }, tr: { tags: { tr: null }, isInline: false, skipLastLineBreak: true, format: '[tr]{0}[/tr]', html: '<tr>{0}</tr>' }, th: { tags: { th: null }, allowsEmpty: true, isInline: false, format: '[th]{0}[/th]', html: '<th>{0}</th>' }, td: { tags: { td: null }, allowsEmpty: true, isInline: false, format: '[td]{0}[/td]', html: '<td>{0}</td>' }, // END_COMMAND // START_COMMAND: Emoticons emoticon: { allowsEmpty: true, tags: { img: { src: null, 'data-sceditor-emoticon': null } }, format: function (element, content) { return attr(element, EMOTICON_DATA_ATTR) + content; }, html: '{0}' }, // END_COMMAND // START_COMMAND: Horizontal Rule hr: { tags: { hr: null }, allowsEmpty: true, isSelfClosing: true, isInline: false, format: '[hr]{0}', html: '<hr />' }, // END_COMMAND // START_COMMAND: Image img: { allowsEmpty: true, tags: { img: { src: null } }, allowedChildren: ['#'], quoteType: QuoteType.never, format: function (element, content) { var width, height, attribs = '', style = function (name) { return element.style ? element.style[name] : null; }; // check if this is an emoticon image if (attr(element, EMOTICON_DATA_ATTR)) { return content; } width = attr(element, 'width') || style('width'); height = attr(element, 'height') || style('height'); // only add width and height if one is specified if ((element.complete && (width || height)) || (width && height)) { attribs = '=' + dom.width(element) + 'x' + dom.height(element); } return '[img' + attribs + ']' + attr(element, 'src') + '[/img]'; }, html: function (token, attrs, content) { var undef, width, height, match, attribs = ''; // handle [img width=340 height=240]url[/img] width = attrs.width; height = attrs.height; // handle [img=340x240]url[/img] if (attrs.defaultattr) { match = attrs.defaultattr.split(/x/i); width = match[0]; height = (match.length === 2 ? match[1] : match[0]); } if (width !== undef) { attribs += ' width="' + escapeEntities(width, true) + '"'; } if (height !== undef) { attribs += ' height="' + escapeEntities(height, true) + '"'; } return '<img' + attribs + ' src="' + escapeUriScheme(content) + '" />'; } }, // END_COMMAND // START_COMMAND: URL url: { allowsEmpty: true, tags: { a: { href: null } }, quoteType: QuoteType.never, format: function (element, content) { var url = attr(element, 'href'); // make sure this link is not an e-mail, // if it is return e-mail BBCode if (url.substr(0, 7) === 'mailto:') { return '[email="' + url.substr(7) + '"]' + content + '[/email]'; } return '[url=' + url + ']' + content + '[/url]'; }, html: function (token, attrs, content) { attrs.defaultattr = escapeEntities(attrs.defaultattr, true) || content; return '<a href="' + escapeUriScheme(attrs.defaultattr) + '">' + content + '</a>'; } }, // END_COMMAND // START_COMMAND: E-mail email: { quoteType: QuoteType.never, html: function (token, attrs, content) { return '<a href="mailto:' + (escapeEntities(attrs.defaultattr, true) || content) + '">' + content + '</a>'; } }, // END_COMMAND // START_COMMAND: Quote quote: { tags: { blockquote: null }, isInline: false, quoteType: QuoteType.never, format: function (element, content) { var authorAttr = 'data-author'; var author = ''; var cite; var children = element.children; for (var i = 0; !cite && i < children.length; i++) { if (is(children[i], 'cite')) { cite = children[i]; } } if (cite || attr(element, authorAttr)) { author = cite && cite.textContent || attr(element, authorAttr); attr(element, authorAttr, author); if (cite) { element.removeChild(cite); } content = this.elementToBbcode(element); author = '=' + author.replace(/(^\s+|\s+$)/g, ''); if (cite) { element.insertBefore(cite, element.firstChild); } } return '[quote' + author + ']' + content + '[/quote]'; }, html: function (token, attrs, content) { if (attrs.defaultattr) { content = '<cite>' + escapeEntities(attrs.defaultattr) + '</cite>' + content; } return '<blockquote>' + content + '</blockquote>'; } }, // END_COMMAND // START_COMMAND: Code code: { tags: { code: null }, isInline: false, allowedChildren: ['#', '#newline'], format: '[code]{0}[/code]', html: '<code>{0}</code>' }, // END_COMMAND // START_COMMAND: Left left: { styles: { 'text-align': [ 'left', '-webkit-left', '-moz-left', '-khtml-left' ] }, isInline: false, allowsEmpty: true, format: '[left]{0}[/left]', html: '<div align="left">{0}</div>' }, // END_COMMAND // START_COMMAND: Centre center: { styles: { 'text-align': [ 'center', '-webkit-center', '-moz-center', '-khtml-center' ] }, isInline: false, allowsEmpty: true, format: '[center]{0}[/center]', html: '<div align="center">{0}</div>' }, // END_COMMAND // START_COMMAND: Right right: { styles: { 'text-align': [ 'right', '-webkit-right', '-moz-right', '-khtml-right' ] }, isInline: false, allowsEmpty: true, format: '[right]{0}[/right]', html: '<div align="right">{0}</div>' }, // END_COMMAND // START_COMMAND: Justify justify: { styles: { 'text-align': [ 'justify', '-webkit-justify', '-moz-justify', '-khtml-justify' ] }, isInline: false, allowsEmpty: true, format: '[justify]{0}[/justify]', html: '<div align="justify">{0}</div>' }, // END_COMMAND // START_COMMAND: YouTube youtube: { allowsEmpty: true, tags: { iframe: { 'data-youtube-id': null } }, format: function (element, content) { element = attr(element, 'data-youtube-id'); return element ? '[youtube]' + element + '[/youtube]' : content; }, html: '<iframe width="560" height="315" frameborder="0" ' + 'src="https://www.youtube-nocookie.com/embed/{0}?wmode=opaque" ' + 'data-youtube-id="{0}" allowfullscreen></iframe>' }, // END_COMMAND // START_COMMAND: Rtl rtl: { styles: { direction: ['rtl'] }, isInline: false, format: '[rtl]{0}[/rtl]', html: '<div style="direction: rtl">{0}</div>' }, // END_COMMAND // START_COMMAND: Ltr ltr: { styles: { direction: ['ltr'] }, isInline: false, format: '[ltr]{0}[/ltr]', html: '<div style="direction: ltr">{0}</div>' }, // END_COMMAND // this is here so that commands above can be removed // without having to remove the , after the last one. // Needed for IE. ignore: {} }; /** * Formats a string replacing {name} with the values of * obj.name properties. * * If there is no property for the specified {name} then * it will be left intact. * * @param {string} str * @param {Object} obj * @return {string} * @since 2.0.0 */ function formatBBCodeString(str, obj) { return str.replace(/\{([^}]+)\}/g, function (match, group) { var undef, escape = true; if (group.charAt(0) === '!') { escape = false; group = group.substring(1); } if (group === '0') { escape = false; } if (obj[group] === undef) { return match; } return escape ? escapeEntities(obj[group], true) : obj[group]; }); } /** * Removes the first and last divs from the HTML. * * This is needed for pasting * @param {string} html * @return {string} * @private */ function removeFirstLastDiv(html) { var node, next, removeDiv, output = document.createElement('div'); removeDiv = function (node, isFirst) { // Don't remove divs that have styling if (dom.hasStyling(node)) { return; } if ((node.childNodes.length !== 1 || !is(node.firstChild, 'br'))) { while ((next = node.firstChild)) { output.insertBefore(next, node); } } if (isFirst) { var lastChild = output.lastChild; if (node !== lastChild && is(lastChild, 'div') && node.nextSibling === lastChild) { output.insertBefore(document.createElement('br'), node); } } output.removeChild(node); }; css(output, 'display', 'none'); output.innerHTML = html.replace(/<\/div>\n/g, '</div>'); if ((node = output.firstChild) && is(node, 'div')) { removeDiv(node, true); } if ((node = output.lastChild) && is(node, 'div')) { removeDiv(node); } return output.innerHTML; } function isFunction(fn) { return typeof fn === 'function'; } /** * Removes any leading or trailing quotes ('") * * @return string * @since v1.4.0 */ function _stripQuotes(str) { return str ? str.replace(/\\(.)/g, '$1').replace(/^(["'])(.*?)\1$/, '$2') : str; } /** * Formats a string replacing {0}, {1}, {2}, ect. with * the params provided * * @param {string} str The string to format * @param {...string} arg The strings to replace * @return {string} * @since v1.4.0 */ function _formatString(str) { var undef; var args = arguments; return str.replace(/\{(\d+)\}/g, function (_, matchNum) { return args[matchNum - 0 + 1] !== undef ? args[matchNum - 0 + 1] : '{' + matchNum + '}'; }); } var TOKEN_OPEN = 'open'; var TOKEN_CONTENT = 'content'; var TOKEN_NEWLINE = 'newline'; var TOKEN_CLOSE = 'close'; /* * @typedef {Object} TokenizeToken * @property {string} type * @property {string} name * @property {string} val * @property {Object.<string, string>} attrs * @property {array} children * @property {TokenizeToken} closing */ /** * Tokenize token object * * @param {string} type The type of token this is, * should be one of tokenType * @param {string} name The name of this token * @param {string} val The originally matched string * @param {array} attrs Any attributes. Only set on * TOKEN_TYPE_OPEN tokens * @param {array} children Any children of this token * @param {TokenizeToken} closing This tokens closing tag. * Only set on TOKEN_TYPE_OPEN tokens * @class {TokenizeToken} * @name {TokenizeToken} * @memberOf BBCodeParser.prototype */ // eslint-disable-next-line max-params function TokenizeToken(type, name, val, attrs, children, closing) { var base = this; base.type = type; base.name = name; base.val = val; base.attrs = attrs || {}; base.children = children || []; base.closing = closing || null; }; TokenizeToken.prototype = { /** @lends BBCodeParser.prototype.TokenizeToken */ /** * Clones this token * * @return {TokenizeToken} */ clone: function () { var base = this; return new TokenizeToken( base.type, base.name, base.val, extend({}, base.attrs), [], base.closing ? base.closing.clone() : null ); }, /** * Splits this token at the specified child * * @param {TokenizeToken} splitAt The child to split at * @return {TokenizeToken} The right half of the split token or * empty clone if invalid splitAt lcoation */ splitAt: function (splitAt) { var offsetLength; var base = this; var clone = base.clone(); var offset = base.children.indexOf(splitAt); if (offset > -1) { // Work out how many items are on the right side of the split // to pass to splice() offsetLength = base.children.length - offset; clone.children = base.children.splice(offset, offsetLength); } return clone; } }; /** * SCEditor BBCode parser class * * @param {Object} options * @class BBCodeParser * @name BBCodeParser * @since v1.4.0 */ function BBCodeParser(options) { var base = this; base.opts = extend({}, BBCodeParser.defaults, options); /** * Takes a BBCode string and splits it into open, * content and close tags. * * It does no checking to verify a tag has a matching open * or closing tag or if the tag is valid child of any tag * before it. For that the tokens should be passed to the * parse function. * * @param {string} str * @return {array} * @memberOf BBCodeParser.prototype */ base.tokenize = function (str) { var matches, type, i; var tokens = []; // The token types in reverse order of precedence // (they're looped in reverse) var tokenTypes = [ { type: TOKEN_CONTENT, regex: /^([^\[\r\n]+|\[)/ }, { type: TOKEN_NEWLINE, regex: /^(\r\n|\r|\n)/ }, { type: TOKEN_OPEN, regex: /^\[[^\[\]]+\]/ }, // Close must come before open as they are // the same except close has a / at the start. { type: TOKEN_CLOSE, regex: /^\[\/[^\[\]]+\]/ } ]; strloop: while (str.length) { i = tokenTypes.length; while (i--) { type = tokenTypes[i].type; // Check if the string matches any of the tokens if (!(matches = str.match(tokenTypes[i].regex)) || !matches[0]) { continue; } // Add the match to the tokens list tokens.push(tokenizeTag(type, matches[0])); // Remove the match from the string str = str.substr(matches[0].length); // The token has been added so start again continue strloop; } // If there is anything left in the string which doesn't match // any of the tokens then just assume it's content and add it. if (str.length) { tokens.push(tokenizeTag(TOKEN_CONTENT, str)); } str = ''; } return tokens; }; /** * Extracts the name an params from a tag * * @param {string} type * @param {string} val * @return {Object} * @private */ function tokenizeTag(type, val) { var matches, attrs, name, openRegex = /\[([^\]\s=]+)(?:([^\]]+))?\]/, closeRegex = /\[\/([^\[\]]+)\]/; // Extract the name and attributes from opening tags and // just the name from closing tags. if (type === TOKEN_OPEN && (matches = val.match(openRegex))) { name = lower(matches[1]); if (matches[2] && (matches[2] = matches[2].trim())) { attrs = tokenizeAttrs(matches[2]); } } if (type === TOKEN_CLOSE && (matches = val.match(closeRegex))) { name = lower(matches[1]); } if (type === TOKEN_NEWLINE) { name = '#newline'; } // Treat all tokens without a name and // all unknown BBCodes as content if (!name || ((type === TOKEN_OPEN || type === TOKEN_CLOSE) && !bbcodeHandlers[name])) { type = TOKEN_CONTENT; name = '#'; } return new TokenizeToken(type, name, val, attrs); } /** * Extracts the individual attributes from a string containing * all the attributes. * * @param {string} attrs * @return {Object} Assoc array of attributes * @private */ function tokenizeAttrs(attrs) { var matches, /* ([^\s=]+) Anything that's not a space or equals = Equals sign = (?: (?: (["']) The opening quote ( (?:\\\2|[^\2])*? Anything that isn't the unescaped opening quote ) \2 The opening quote again which will close the string ) | If not a quoted string then match ( (?:.(?!\s\S+=))*.? Anything that isn't part of [space][non-space][=] which would be a new attribute ) ) */ attrRegex = /([^\s=]+)=(?:(?:(["'])((?:\\\2|[^\2])*?)\2)|((?:.(?!\s\S+=))*.))/g, ret = {}; // if only one attribute then remove the = from the start and // strip any quotes if (attrs.charAt(0) === '=' && attrs.indexOf('=', 1) < 0) { ret.defaultattr = _stripQuotes(attrs.substr(1)); } else { if (attrs.charAt(0) === '=') { attrs = 'defaultattr' + attrs; } // No need to strip quotes here, the regex will do that. while ((matches = attrRegex.exec(attrs))) { ret[lower(matches[1])] = _stripQuotes(matches[3]) || matches[4]; } } return ret; } /** * Parses a string into an array of BBCodes * * @param {string} str * @param {boolean} preserveNewLines If to preserve all new lines, not * strip any based on the passed * formatting options * @return {array} Array of BBCode objects * @memberOf BBCodeParser.prototype */ base.parse = function (str, preserveNewLines) { var ret = parseTokens(base.tokenize(str)); var opts = base.opts; if (opts.fixInvalidNesting) { fixNesting(ret); } normaliseNewLines(ret, null, preserveNewLines); if (opts.removeEmptyTags) { removeEmpty(ret); } return ret; }; /** * Checks if an array of TokenizeToken's contains the * specified token. * * Checks the tokens name and type match another tokens * name and type in the array. * * @param {string} name * @param {string} type * @param {array} arr * @return {Boolean} * @private */ function hasTag(name, type, arr) { var i = arr.length; while (i--) { if (arr[i].type === type && arr[i].name === name) { return true; } } return false; } /** * Checks if the child tag is allowed as one * of the parent tags children. * * @param {TokenizeToken} parent * @param {TokenizeToken} child * @return {Boolean} * @private */ function isChildAllowed(parent, child) { var parentBBCode = parent ? bbcodeHandlers[parent.name] : {}, allowedChildren = parentBBCode.allowedChildren; if (base.opts.fixInvalidChildren && allowedChildren) { return allowedChildren.indexOf(child.name || '#') > -1; } return true; } // TODO: Tidy this parseTokens() function up a bit. /** * Parses an array of tokens created by tokenize() * * @param {array} toks * @return {array} Parsed tokens * @see tokenize() * @private */ function parseTokens(toks) { var token, bbcode, curTok, clone, i, next, cloned = [], output = [], openTags = [], /** * Returns the currently open tag or undefined * @return {TokenizeToken} */ currentTag = function () { return last(openTags); }, /** * Adds a tag to either the current tags children * or to the output array. * @param {TokenizeToken} token * @private */ addTag = function (token) { if (currentTag()) { currentTag().children.push(token); } else { output.push(token); } }, /** * Checks if this tag closes the current tag * @param {string} name * @return {Void} */ closesCurrentTag = function (name) { return currentTag() && (bbcode = bbcodeHandlers[currentTag().name]) && bbcode.closedBy && bbcode.closedBy.indexOf(name) > -1; }; while ((token = toks.shift())) { next = toks[0]; /* * Fixes any invalid children. * * If it is an element which isn't allowed as a child of it's * parent then it will be converted to content of the parent * element. i.e. * [code]Code [b]only[/b] allows text.[/code] * Will become: * <code>Code [b]only[/b] allows text.</code> * Instead of: * <code>Code <b>only</b> allows text.</code> */ // Ignore tags that can't be children if (!isChildAllowed(currentTag(), token)) { // exclude closing tags of current tag if (token.type !== TOKEN_CLOSE || !currentTag() || token.name !== currentTag().name) { token.name = '#'; token.type = TOKEN_CONTENT; } } switch (token.type) { case TOKEN_OPEN: // Check it this closes a parent, // e.g. for lists [*]one [*]two if (closesCurrentTag(token.name)) { openTags.pop(); } addTag(token); bbcode = bbcodeHandlers[token.name]; // If this tag is not self closing and it has a closing // tag then it is open and has children so add it to the // list of open tags. If has the closedBy property then // it is closed by other tags so include everything as // it's children until one of those tags is reached. if (bbcode && !bbcode.isSelfClosing && (bbcode.closedBy || hasTag(token.name, TOKEN_CLOSE, toks))) { openTags.push(token); } else if (!bbcode || !bbcode.isSelfClosing) { token.type = TOKEN_CONTENT; } break; case TOKEN_CLOSE: // check if this closes the current tag, // e.g. [/list] would close an open [*] if (currentTag() && token.name !== currentTag().name && closesCurrentTag('/' + token.name)) { openTags.pop(); } // If this is closing the currently open tag just pop // the close tag off the open tags array if (currentTag() && token.name === currentTag().name) { currentTag().closing = token; openTags.pop(); // If this is closing an open tag that is the parent of // the current tag then clone all the tags including the // current one until reaching the parent that is being // closed. Close the parent and then add the clones back // in. } else if (hasTag(token.name, TOKEN_OPEN, openTags)) { // Remove the tag from the open tags while ((curTok = openTags.pop())) { // If it's the tag that is being closed then // discard it and break the loop. if (curTok.name === token.name) { curTok.closing = token; break; } // Otherwise clone this tag and then add any // previously cloned tags as it's children clone = curTok.clone(); if (cloned.length) { clone.children.push(last(cloned)); } cloned.push(clone); } // Place block linebreak before cloned tags if (next && next.type === TOKEN_NEWLINE) { bbcode = bbcodeHandlers[token.name]; if (bbcode && bbcode.isInline === false) { addTag(next); toks.shift(); } } // Add the last cloned child to the now current tag // (the parent of the tag which was being closed) addTag(last(cloned)); // Add all the cloned tags to the open tags list i = cloned.length; while (i--) { openTags.push(cloned[i]); } cloned.length = 0; // This tag is closing nothing so treat it as content } else { token.type = TOKEN_CONTENT; addTag(token); } break; case TOKEN_NEWLINE: // handle things like // [*]list\nitem\n[*]list1 // where it should come out as // [*]list\nitem[/*]\n[*]list1[/*] // instead of // [*]list\nitem\n[/*][*]list1[/*] if (currentTag() && next && closesCurrentTag( (next.type === TOKEN_CLOSE ? '/' : '') + next.name )) { // skip if the next tag is the closing tag for // the option tag, i.e. [/*] if (!(next.type === TOKEN_CLOSE && next.name === currentTag().name)) { bbcode = bbcodeHandlers[currentTag().name]; if (bbcode && bbcode.breakAfter) { openTags.pop(); } else if (bbcode && bbcode.isInline === false && base.opts.breakAfterBlock && bbcode.breakAfter !== false) { openTags.pop(); } } } addTag(token); break; default: // content addTag(token); break; } } return output; } /** * Normalise all new lines * * Removes any formatting new lines from the BBCode * leaving only content ones. I.e. for a list: * * [list] * [*] list item one * with a line break * [*] list item two * [/list] * * would become * * [list] [*] list item one * with a line break [*] list item two [/list] * * Which makes it easier to convert to HTML or add * the formatting new lines back in when converting * back to BBCode * * @param {array} children * @param {TokenizeToken} parent * @param {boolean} onlyRemoveBreakAfter * @return {void} */ function normaliseNewLines(children, parent, onlyRemoveBreakAfter) { var token, left, right, parentBBCode, bbcode, removedBreakEnd, removedBreakBefore, remove; var childrenLength = children.length; // TODO: this function really needs tidying up if (parent) { parentBBCode = bbcodeHandlers[parent.name]; } var i = childrenLength; while (i--) { if (!(token = children[i])) { continue; } if (token.type === TOKEN_NEWLINE) { left = i > 0 ? children[i - 1] : null; right = i < childrenLength - 1 ? children[i + 1] : null; remove = false; // Handle the start and end new lines // e.g. [tag]\n and \n[/tag] if (!onlyRemoveBreakAfter && parentBBCode && parentBBCode.isSelfClosing !== true) { // First child of parent so must be opening line break // (breakStartBlock, breakStart) e.g. [tag]\n if (!left) { if (parentBBCode.isInline === false && base.opts.breakStartBlock && parentBBCode.breakStart !== false) { remove = true; } if (parentBBCode.breakStart) { remove = true; } // Last child of parent so must be end line break // (breakEndBlock, breakEnd) // e.g. \n[/tag] // remove last line break (breakEndBlock, breakEnd) } else if (!removedBreakEnd && !right) { if (parentBBCode.isInline === false && base.opts.breakEndBlock && parentBBCode.breakEnd !== false) { remove = true; } if (parentBBCode.breakEnd) { remove = true; } removedBreakEnd = remove; } } if (left && left.type === TOKEN_OPEN) { if ((bbcode = bbcodeHandlers[left.name])) { if (!onlyRemoveBreakAfter) { if (bbcode.isInline === false && base.opts.breakAfterBlock && bbcode.breakAfter !== false) { remove = true; } if (bbcode.breakAfter) { remove = true; } } else if (bbcode.isInline === false) { remove = true; } } } if (!onlyRemoveBreakAfter && !removedBreakBefore && right && right.type === TOKEN_OPEN) { if ((bbcode = bbcodeHandlers[right.name])) { if (bbcode.isInline === false && base.opts.breakBeforeBlock && bbcode.breakBefore !== false) { remove = true; } if (bbcode.breakBefore) { remove = true; } removedBreakBefore = remove; if (remove) { children.splice(i, 1); continue; } } } if (remove) { children.splice(i, 1); } // reset double removedBreakBefore removal protection. // This is needed for cases like \n\n[\tag] where // only 1 \n should be removed but without this they both // would be. removedBreakBefore = false; } else if (token.type === TOKEN_OPEN) { normaliseNewLines(token.children, token, onlyRemoveBreakAfter); } } } /** * Fixes any invalid nesting. * * If it is a block level element inside 1 or more inline elements * then those inline elements will be split at the point where the * block level is and the block level element placed between the split * parts. i.e. * [inline]A[blocklevel]B[/blocklevel]C[/inline] * Will become: * [inline]A[/inline][blocklevel]B[/blocklevel][inline]C[/inline] * * @param {array} children * @param {array} [parents] Null if there is no parents * @param {boolea} [insideInline] If inside an inline element * @param {array} [rootArr] Root array if there is one * @return {array} * @private */ function fixNesting(children, parents, insideInline, rootArr) { var token, i, parent, parentIndex, parentParentChildren, right; var isInline = function (token) { var bbcode = bbcodeHandlers[token.name]; return !bbcode || bbcode.isInline !== false; }; parents = parents || []; rootArr = rootArr || children; // This must check the length each time as it can change when // tokens are moved to fix the nesting. for (i = 0; i < children.length; i++) { if (!(token = children[i]) || token.type !== TOKEN_OPEN) { continue; } if (insideInline && !isInline(token)) { // if this is a blocklevel element inside an inline one then // split the parent at the block level element parent = last(parents); right = parent.splitAt(token); parentParentChildren = parents.length > 1 ? parents[parents.length - 2].children : rootArr; // If parent inline is allowed inside this tag, clone it and // wrap this tags children in it. if (isChildAllowed(token, parent)) { var clone = parent.clone(); clone.children = token.children; token.children = [clone]; } parentIndex = parentParentChildren.indexOf(parent); if (parentIndex > -1) { // remove the block level token from the right side of // the split inline element right.children.splice(0, 1); // insert the block level token and the right side after // the left side of the inline token parentParentChildren.splice( parentIndex + 1, 0, token, right ); // If token is a block and is followed by a newline, // then move the newline along with it to the new parent var next = right.children[0]; if (next && next.type === TOKEN_NEWLINE) { if (!isInline(token)) { right.children.splice(0, 1); parentParentChildren.splice( parentIndex + 2, 0, next ); } } // return to parents loop as the // children have now increased return; } } parents.push(token); fixNesting( token.children, parents, insideInline || isInline(token), rootArr ); parents.pop(); } } /** * Removes any empty BBCodes which are not allowed to be empty. * * @param {array} tokens * @private */ function removeEmpty(tokens) { var token, bbcode; /** * Checks if all children are whitespace or not * @private */ var isTokenWhiteSpace = function (children) { var j = children.length; while (j--) { var type = children[j].type; if (type === TOKEN_OPEN || type === TOKEN_CLOSE) { return false; } if (type === TOKEN_CONTENT && /\S|\u00A0/.test(children[j].val)) { return false; } } return true; }; var i = tokens.length; while (i--) { // So skip anything that isn't a tag since only tags can be // empty, content can't if (!(token = tokens[i]) || token.type !== TOKEN_OPEN) { continue; } bbcode = bbcodeHandlers[token.name]; // Remove any empty children of this tag first so that if they // are all removed this one doesn't think it's not empty. removeEmpty(token.children); if (isTokenWhiteSpace(token.children) && bbcode && !bbcode.isSelfClosing && !bbcode.allowsEmpty) { tokens.splice.apply(tokens, [i, 1].concat(token.children)); } } } /** * Converts a BBCode string to HTML * * @param {string} str * @param {boolean} preserveNewLines If to preserve all new lines, not * strip any based on the passed * formatting options * @return {string} * @memberOf BBCodeParser.prototype */ base.toHTML = function (str, preserveNewLines) { return convertToHTML(base.parse(str, preserveNewLines), true); }; /** * @private */ function convertToHTML(tokens, isRoot) { var undef, token, bbcode, content, html, needsBlockWrap, blockWrapOpen, isInline, lastChild, ret = ''; isInline = function (bbcode) { return (!bbcode || (bbcode.isHtmlInline !== undef ? bbcode.isHtmlInline : bbcode.isInline)) !== false; }; while (tokens.length > 0) { if (!(token = tokens.shift())) { continue; } if (token.type === TOKEN_OPEN) { lastChild = token.children[token.children.length - 1] || {}; bbcode = bbcodeHandlers[token.name]; needsBlockWrap = isRoot && isInline(bbcode); content = convertToHTML(token.children, false); if (bbcode && bbcode.html) { // Only add a line break to the end if this is // blocklevel and the last child wasn't block-level if (!isInline(bbcode) && isInline(bbcodeHandlers[lastChild.name]) && !bbcode.isPreFormatted && !bbcode.skipLastLineBreak) { // Add placeholder br to end of block level // elements content += '<br />'; } if (!isFunction(bbcode.html)) { token.attrs['0'] = content; html = formatBBCodeString( bbcode.html, token.attrs ); } else { html = bbcode.html.call( base, token, token.attrs, content ); } } else { html = token.val + content + (token.closing ? token.closing.val : ''); } } else if (token.type === TOKEN_NEWLINE) { if (!isRoot) { ret += '<br />'; continue; } // If not already in a block wrap then start a new block if (!blockWrapOpen) { ret += '<div>'; } ret += '<br />'; // Normally the div acts as a line-break with by moving // whatever comes after onto a new line. // If this is the last token, add an extra line-break so it // shows as there will be nothing after it. if (!tokens.length) { ret += '<br />'; } ret += '</div>\n'; blockWrapOpen = false; continue; // content } else { needsBlockWrap = isRoot; html = escapeEntities(token.val, true); } if (needsBlockWrap && !blockWrapOpen) { ret += '<div>'; blockWrapOpen = true; } else if (!needsBlockWrap && blockWrapOpen) { ret += '</div>\n'; blockWrapOpen = false; } ret += html; } if (blockWrapOpen) { ret += '</div>\n'; } return ret; } /** * Takes a BBCode string, parses it then converts it back to BBCode. * * This will auto fix the BBCode and format it with the specified * options. * * @param {string} str * @param {boolean} preserveNewLines If to preserve all new lines, not * strip any based on the passed * formatting options * @return {string} * @memberOf BBCodeParser.prototype */ base.toBBCode = function (str, preserveNewLines) { return convertToBBCode(base.parse(str, preserveNewLines)); }; /** * Converts parsed tokens back into BBCode with the * formatting specified in the options and with any * fixes specified. * * @param {array} toks Array of parsed tokens from base.parse() * @return {string} * @private */ function convertToBBCode(toks) { var token, attr, bbcode, isBlock, isSelfClosing, quoteType, breakBefore, breakStart, breakEnd, breakAfter, ret = ''; while (toks.length > 0) { if (!(token = toks.shift())) { continue; } // TODO: tidy this bbcode = bbcodeHandlers[token.name]; isBlock = !(!bbcode || bbcode.isInline !== false); isSelfClosing = bbcode && bbcode.isSelfClosing; breakBefore = (isBlock && base.opts.breakBeforeBlock && bbcode.breakBefore !== false) || (bbcode && bbcode.breakBefore); breakStart = (isBlock && !isSelfClosing && base.opts.breakStartBlock && bbcode.breakStart !== false) || (bbcode && bbcode.breakStart); breakEnd = (isBlock && base.opts.breakEndBlock && bbcode.breakEnd !== false) || (bbcode && bbcode.breakEnd); breakAfter = (isBlock && base.opts.breakAfterBlock && bbcode.breakAfter !== false) || (bbcode && bbcode.breakAfter); quoteType = (bbcode ? bbcode.quoteType : null) || base.opts.quoteType || QuoteType.auto; if (!bbcode && token.type === TOKEN_OPEN) { ret += token.val; if (token.children) { ret += convertToBBCode(token.children); } if (token.closing) { ret += token.closing.val; } } else if (token.type === TOKEN_OPEN) { if (breakBefore) { ret += '\n'; } // Convert the tag and it's attributes to BBCode ret += '[' + token.name; if (token.attrs) { if (token.attrs.defaultattr) { ret += '=' + quote( token.attrs.defaultattr, quoteType, 'defaultattr' ); delete token.attrs.defaultattr; } for (attr in token.attrs) { if (token.attrs.hasOwnProperty(attr)) { ret += ' ' + attr + '=' + quote(token.attrs[attr], quoteType, attr); } } } ret += ']'; if (breakStart) { ret += '\n'; } // Convert the tags children to BBCode if (token.children) { ret += convertToBBCode(token.children); } // add closing tag if not self closing if (!isSelfClosing && !bbcode.excludeClosing) { if (breakEnd) { ret += '\n'; } ret += '[/' + token.name + ']'; } if (breakAfter) { ret += '\n'; } // preserve whatever was recognized as the // closing tag if it is a self closing tag if (token.closing && isSelfClosing) { ret += token.closing.val; } } else { ret += token.val; } } return ret; } /** * Quotes an attribute * * @param {string} str * @param {BBCodeParser.QuoteType} quoteType * @param {string} name * @return {string} * @private */ function quote(str, quoteType, name) { var needsQuotes = /\s|=/.test(str); if (isFunction(quoteType)) { return quoteType(str, name); } if (quoteType === QuoteType.never || (quoteType === QuoteType.auto && !needsQuotes)) { return str; } return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; } /** * Returns the last element of an array or null * * @param {array} arr * @return {Object} Last element * @private */ function last(arr) { if (arr.length) { return arr[arr.length - 1]; } return null; } /** * Converts a string to lowercase. * * @param {string} str * @return {string} Lowercase version of str * @private */ function lower(str) { return str.toLowerCase(); } }; /** * Quote type * @type {Object} * @class QuoteType * @name BBCodeParser.QuoteType * @since 1.4.0 */ BBCodeParser.QuoteType = QuoteType; /** * Default BBCode parser options * @type {Object} */ BBCodeParser.defaults = { /** * If to add a new line before block level elements * * @type {Boolean} */ breakBeforeBlock: false, /** * If to add a new line after the start of block level elements * * @type {Boolean} */ breakStartBlock: false, /** * If to add a new line before the end of block level elements * * @type {Boolean} */ breakEndBlock: false, /** * If to add a new line after block level elements * * @type {Boolean} */ breakAfterBlock: true, /** * If to remove empty tags * * @type {Boolean} */ removeEmptyTags: true, /** * If to fix invalid nesting, * i.e. block level elements inside inline elements. * * @type {Boolean} */ fixInvalidNesting: true, /** * If to fix invalid children. * i.e. A tag which is inside a parent that doesn't * allow that type of tag. * * @type {Boolean} */ fixInvalidChildren: true, /** * Attribute quote type * * @type {BBCodeParser.QuoteType} * @since 1.4.1 */ quoteType: QuoteType.auto, /** * Whether to use strict matching on attributes and styles. * * When true this will perform AND matching requiring all tag * attributes and styles to match. * * When false will perform OR matching and will match if any of * a tags attributes or styles match. * * @type {Boolean} * @since 3.1.0 */ strictMatch: false }; /** * Converts a number 0-255 to hex. * * Will return 00 if number is not a valid number. * * @param {any} number * @return {string} * @private */ function toHex(number) { number = parseInt(number, 10); if (isNaN(number)) { return '00'; } number = Math.max(0, Math.min(number, 255)).toString(16); return number.length < 2 ? '0' + number : number; } /** * Normalises a CSS colour to hex #xxxxxx format * * @param {string} colorStr * @return {string} * @private */ function _normaliseColour(colorStr) { var match; colorStr = colorStr || '#000'; // rgb(n,n,n); if ((match = colorStr.match(/rgb\((\d{1,3}),\s*?(\d{1,3}),\s*?(\d{1,3})\)/i))) { return '#' + toHex(match[1]) + toHex(match[2]) + toHex(match[3]); } // expand shorthand if ((match = colorStr.match(/#([0-f])([0-f])([0-f])\s*?$/i))) { return '#' + match[1] + match[1] + match[2] + match[2] + match[3] + match[3]; } return colorStr; } /** * SCEditor BBCode format * @since 2.0.0 */ function bbcodeFormat() { var base = this; base.stripQuotes = _stripQuotes; /** * cache of all the tags pointing to their bbcodes to enable * faster lookup of which bbcode a tag should have * @private */ var tagsToBBCodes = {}; /** * Allowed children of specific HTML tags. Empty array if no * children other than text nodes are allowed * @private */ var validChildren = { ul: ['li', 'ol', 'ul'], ol: ['li', 'ol', 'ul'], table: ['tr'], tr: ['td', 'th'], code: ['br', 'p', 'div'] }; /** * Populates tagsToBBCodes and stylesToBBCodes for easier lookups * * @private */ function buildBbcodeCache() { each(bbcodeHandlers, function (bbcode, handler) { var isBlock = handler.isInline === false, tags = bbcodeHandlers[bbcode].tags, styles = bbcodeHandlers[bbcode].styles; if (styles) { tagsToBBCodes['*'] = tagsToBBCodes['*'] || {}; tagsToBBCodes['*'][isBlock] = tagsToBBCodes['*'][isBlock] || {}; tagsToBBCodes['*'][isBlock][bbcode] = [ ['style', Object.entries(styles)] ]; } if (tags) { each(tags, function (tag, values) { if (values && values.style) { values.style = Object.entries(values.style); } tagsToBBCodes[tag] = tagsToBBCodes[tag] || {}; tagsToBBCodes[tag][isBlock] = tagsToBBCodes[tag][isBlock] || {}; tagsToBBCodes[tag][isBlock][bbcode] = values && Object.entries(values); }); } }); }; /** * Handles adding newlines after block level elements * * @param {HTMLElement} element The element to convert * @param {string} content The tags text content * @return {string} * @private */ function handleBlockNewlines(element, content) { var tag = element.nodeName.toLowerCase(); var isInline = dom.isInline; if (!isInline(element, true) || tag === 'br') { var isLastBlockChild, parent, parentLastChild, previousSibling = element.previousSibling; // Skips selection makers and ignored elements // Skip empty inline elements while (previousSibling && previousSibling.nodeType === 1 && !is(previousSibling, 'br') && isInline(previousSibling, true) && !previousSibling.firstChild) { previousSibling = previousSibling.previousSibling; } // If it's the last block of an inline that is the last // child of a block then it shouldn't cause a line break // <block><inline><br></inline></block> do { parent = element.parentNode; parentLastChild = parent && parent.lastChild; isLastBlockChild = parentLastChild === element; element = parent; } while (parent && isLastBlockChild && isInline(parent, true)); // If this block is: // * Not the last child of a block level element // * Is a <li> tag (lists are blocks) if (!isLastBlockChild || tag === 'li') { content += '\n'; } // Check for: // <block>text<block>text</block></block> // // The second opening <block> opening tag should cause a // line break because the previous sibing is inline. if (tag !== 'br' && previousSibling && !is(previousSibling, 'br') && isInline(previousSibling, true)) { content = '\n' + content; } } return content; } /** * Handles a HTML tag and finds any matching BBCodes * * @param {HTMLElement} element The element to convert * @param {string} content The Tags text content * @param {boolean} blockLevel * @return {string} Content with any matching BBCode tags * wrapped around it. * @private */ function handleTags(element, content, blockLevel) { function isStyleMatch(style) { var property = style[0]; var values = style[1]; var val = dom.getStyle(element, property); var parent = element.parentNode; // if the parent has the same style use that instead of this one // so you don't end up with [i]parent[i]child[/i][/i] if (!val || parent && dom.hasStyle(parent, property, val)) { return false; } return !values || values.includes(val); } function createAttributeMatch(isStrict) { return function (attribute) { var name = attribute[0]; var value = attribute[1]; // code tags should skip most styles if (name === 'style' && element.nodeName === 'CODE') { return false; } if (name === 'style' && value) { return value[isStrict ? 'every' : 'some'](isStyleMatch); } else { var val = attr(element, name); return val && (!value || value.includes(val)); } }; } function handleTag(tag) { if (!tagsToBBCodes[tag] || !tagsToBBCodes[tag][blockLevel]) { return; } // loop all bbcodes for this tag each(tagsToBBCodes[tag][blockLevel], function (bbcode, attrs) { var fn, format, isStrict = bbcodeHandlers[bbcode].strictMatch; if (typeof isStrict === 'undefined') { isStrict = base.opts.strictMatch; } // Skip if the element doesn't have the attribute or the // attribute doesn't match one of the required values fn = isStrict ? 'every' : 'some'; if (attrs && !attrs[fn](createAttributeMatch(isStrict))) { return; } format = bbcodeHandlers[bbcode].format; if (isFunction(format)) { content = format.call(base, element, content); } else { content = _formatString(format, content); } return false; }); } handleTag('*'); handleTag(element.nodeName.toLowerCase()); return content; } /** * Converts a HTML dom element to BBCode starting from * the innermost element and working backwards * * @private * @param {HTMLElement} element * @return {string} BBCode * @memberOf SCEditor.plugins.bbcode.prototype */ function elementToBbcode(element) { var toBBCode = function (node, vChildren) { var ret = ''; dom.traverse(node, function (node) { var content = '', nodeType = node.nodeType, tag = node.nodeName.toLowerCase(), vChild = validChildren[tag], firstChild = node.firstChild, isValidChild = true; if (typeof vChildren === 'object') { isValidChild = vChildren.indexOf(tag) > -1; // Emoticons should always be converted if (is(node, 'img') && attr(node, EMOTICON_DATA_ATTR)) { isValidChild = true; } // if this tag is one of the parents allowed children // then set this tags allowed children to whatever it // allows, otherwise set to what the parent allows if (!isValidChild) { vChild = vChildren; } } // 3 = text and 1 = element if (nodeType !== 3 && nodeType !== 1) { return; } if (nodeType === 1) { // skip empty nlf elements (new lines automatically // added after block level elements like quotes) if (is(node, '.sceditor-nlf') && !firstChild) { return; } // don't convert iframe contents if (tag !== 'iframe') { content = toBBCode(node, vChild); } // TODO: isValidChild is no longer needed. Should use // valid children bbcodes instead by creating BBCode // tokens like the parser. if (isValidChild) { // code tags should skip most styles if (tag !== 'code') { // First parse inline codes content = handleTags(node, content, false); } content = handleTags(node, content, true); ret += handleBlockNewlines(node, content); } else { ret += content; } } else { ret += node.nodeValue; } }, false, true); return ret; }; return toBBCode(element); }; /** * Initializer * @private */ base.init = function () { base.opts = this.opts; base.elementToBbcode = elementToBbcode; // build the BBCode cache buildBbcodeCache(); this.commands = extend( true, {}, defaultCommandsOverrides, this.commands ); // Add BBCode helper methods this.toBBCode = base.toSource; this.fromBBCode = base.toHtml; }; /** * Converts BBCode into HTML * * @param {boolean} asFragment * @param {string} source * @param {boolean} [legacyAsFragment] Used by fromBBCode() method */ function toHtml(asFragment, source, legacyAsFragment) { var parser = new BBCodeParser(base.opts.parserOptions); var html = parser.toHTML( base.opts.bbcodeTrim ? source.trim() : source ); return (asFragment || legacyAsFragment) ? removeFirstLastDiv(html) : html; } /** * Converts HTML into BBCode * * @param {boolean} asFragment * @param {string} html * @param {!Document} [context] * @param {!HTMLElement} [parent] * @return {string} * @private */ function toSource(asFragment, html, context, parent) { context = context || document; var bbcode, elements; var containerParent = context.createElement('div'); var container = context.createElement('div'); var parser = new BBCodeParser(base.opts.parserOptions); container.innerHTML = html; css(containerParent, 'visibility', 'hidden'); containerParent.appendChild(container); context.body.appendChild(containerParent); if (asFragment) { // Add text before and after so removeWhiteSpace doesn't remove // leading and trailing whitespace containerParent.insertBefore( context.createTextNode('#'), containerParent.firstChild ); containerParent.appendChild(context.createTextNode('#')); } // Match parents white-space handling if (parent) { css(container, 'whiteSpace', css(parent, 'whiteSpace')); } // Remove all nodes with sceditor-ignore class elements = container.getElementsByClassName('sceditor-ignore'); while (elements.length) { elements[0].parentNode.removeChild(elements[0]); } dom.removeWhiteSpace(containerParent); bbcode = elementToBbcode(container); context.body.removeChild(containerParent); bbcode = parser.toBBCode(bbcode, true); if (base.opts.bbcodeTrim) { bbcode = bbcode.trim(); } return bbcode; }; base.toHtml = toHtml.bind(null, false); base.fragmentToHtml = toHtml.bind(null, true); base.toSource = toSource.bind(null, false); base.fragmentToSource = toSource.bind(null, true); }; /** * Gets a BBCode * * @param {string} name * @return {Object|null} * @since 2.0.0 */ bbcodeFormat.get = function (name) { return bbcodeHandlers[name] || null; }; /** * Adds a BBCode to the parser or updates an existing * BBCode if a BBCode with the specified name already exists. * * @param {string} name * @param {Object} bbcode * @return {this} * @since 2.0.0 */ bbcodeFormat.set = function (name, bbcode) { if (name && bbcode) { // merge any existing command properties bbcode = extend(bbcodeHandlers[name] || {}, bbcode); bbcode.remove = function () { delete bbcodeHandlers[name]; }; bbcodeHandlers[name] = bbcode; } return this; }; /** * Renames a BBCode * * This does not change the format or HTML handling, those must be * changed manually. * * @param {string} name [description] * @param {string} newName [description] * @return {this|false} * @since 2.0.0 */ bbcodeFormat.rename = function (name, newName) { if (name in bbcodeHandlers) { bbcodeHandlers[newName] = bbcodeHandlers[name]; delete bbcodeHandlers[name]; } return this; }; /** * Removes a BBCode * * @param {string} name * @return {this} * @since 2.0.0 */ bbcodeFormat.remove = function (name) { if (name in bbcodeHandlers) { delete bbcodeHandlers[name]; } return this; }; bbcodeFormat.formatBBCodeString = formatBBCodeString; sceditor.formats.bbcode = bbcodeFormat; sceditor.BBCodeParser = BBCodeParser; }(sceditor));