/**
* SCEditor BBCode Plugin
* http://www.sceditor.com/
*
* Copyright (C) 2011-2014, Sam Clarke (samclarke.com)
*
* SCEditor is licensed under the MIT license:
* http://www.opensource.org/licenses/mit-license.php
*
* @fileoverview SCEditor BBCode Plugin
* @author Sam Clarke
* @requires jQuery
*/
/*global prompt: true*/
/*jshint maxdepth: false*/
// TODO: Tidy this code up and consider seperating the BBCode parser into a
// standalone module that can be used with other JS/NodeJS
(function ($, window, document) {
'use strict';
var SCEditor = $.sceditor;
var sceditorPlugins = SCEditor.plugins;
var escapeEntities = SCEditor.escapeEntities;
var escapeUriScheme = SCEditor.escapeUriScheme;
var IE_VER = SCEditor.ie;
// In IE < 11 a BR at the end of a block level element
// causes a double line break.
var IE_BR_FIX = IE_VER && IE_VER < 11;
var getEditorCommand = SCEditor.command.get;
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) {
var content = '';
$.each(selected.split(/\r?\n/), function () {
content += (content ? '\n' : '') +
'[li]' + this + '[/li]';
});
this.insertText('[ul]\n' + content + '\n[/ul]');
}
},
orderedlist: {
txtExec: function (caller, selected) {
var content = '';
$.each(selected.split(/\r?\n/), function () {
content += (content ? '\n' : '') +
'[li]' + this + '[/li]';
});
sceditorPlugins.bbcode.bbcode.get('');
this.insertText('[ol]\n' + content + '\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,
url = prompt(editor._('Enter the image URL:'), selected);
if (url) {
editor.insertText('[img]' + url + '[/img]');
}
}
},
email: {
txtExec: function (caller, selected) {
var editor = this,
display = selected && selected.indexOf('@') > -1 ?
null : selected,
email = prompt(editor._('Enter the e-mail address:'),
(display ? '' : selected)),
text = prompt(editor._('Enter the displayed text:'),
display || email) || email;
if (email) {
editor.insertText('[email=' + email + ']' +
text + '[/email]');
}
}
},
link: {
txtExec: function (caller, selected) {
var editor = this,
display = /^[a-z]+:\/\//i.test($.trim(selected)) ?
null : selected,
url = prompt(editor._('Enter URL:'),
(display ? 'http://' : $.trim(selected))),
text = prompt(editor._('Enter the displayed text:'),
display || url) || url;
if (url) {
editor.insertText('[url=' + url + ']' + text + '[/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]']
}
};
/**
* Removes any leading or trailing quotes ('")
*
* @return string
* @since v1.4.0
*/
var _stripQuotes = function (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} args... The strings to replace
* @return {String}
* @since v1.4.0
*/
var _formatString = function () {
var undef;
var args = arguments;
return args[0].replace(/\{(\d+)\}/g, function (str, p1) {
return args[p1 - 0 + 1] !== undef ?
args[p1 - 0 + 1] :
'{' + p1 + '}';
});
};
/**
* Enum of valid token types
* @type {Object}
* @private
*/
var TokenType = {
OPEN: 'open',
CONTENT: 'content',
NEWLINE: 'newline',
CLOSE: 'close'
};
/**
* 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
* TokenType.OPEN tokens
* @param {Array} children Any children of this token
* @param {TokenizeToken} closing This tokens closing tag.
* Only set on TokenType.OPEN tokens
* @class TokenizeToken
* @name TokenizeToken
* @memberOf jQuery.sceditor.BBCodeParser.prototype
*/
var TokenizeToken = function (
/*jshint maxparams: false*/
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 jQuery.sceditor.BBCodeParser.prototype.TokenizeToken */
/**
* Clones this token
*
* @param {Bool} includeChildren If to include the children in
* the clone. Defaults to false.
* @return {TokenizeToken}
*/
clone: function (includeChildren) {
var base = this;
return new TokenizeToken(
base.type,
base.name,
base.val,
base.attrs,
includeChildren ? base.children : [],
base.closing ? base.closing.clone() : null
);
},
/**
* Splits this token at the specified child
*
* @param {TokenizeToken|Int} splitAt The child to split at or the
* index of the child
* @return {TokenizeToken} The right half of the split token or
* null if failed
*/
splitAt: function (splitAt) {
var clone;
var base = this;
var splitAtLength = 0;
var childrenLen = base.children.length;
if (typeof splitAt !== 'number') {
splitAt = $.inArray(splitAt, base.children);
}
if (splitAt < 0 || splitAt > childrenLen) {
return null;
}
// Work out how many items are on the right side of the split
// to pass to splice()
while (childrenLen--) {
if (childrenLen >= splitAt) {
splitAtLength++;
} else {
childrenLen = 0;
}
}
clone = base.clone();
clone.children = base.children.splice(splitAt, splitAtLength);
return clone;
}
};
/**
* SCEditor BBCode parser class
*
* @param {Object} options
* @class BBCodeParser
* @name jQuery.sceditor.BBCodeParser
* @since v1.4.0
*/
var BBCodeParser = function (options) {
// make sure this is not being called as a function
if (!(this instanceof BBCodeParser)) {
return new BBCodeParser(options);
}
var base = this;
// Private methods
var init,
tokenizeTag,
tokenizeAttrs,
parseTokens,
normaliseNewLines,
fixNesting,
isChildAllowed,
removeEmpty,
fixChildren,
convertToHTML,
convertToBBCode,
hasTag,
quote,
lower,
last;
init = function () {
base.bbcodes = sceditorPlugins.bbcode.bbcodes;
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 jQuery.sceditor.BBCodeParser.prototype
*/
base.tokenize = function (str) {
var matches, type, i;
var toks = [];
var tokens = [
// Close must come before open as they are
// the same except close has a / at the start.
{
type: TokenType.CLOSE,
regex: /^\[\/[^\[\]]+\]/
},
{
type: TokenType.OPEN,
regex: /^\[[^\[\]]+\]/
},
{
type: TokenType.NEWLINE,
regex: /^(\r\n|\r|\n)/
},
{
type: TokenType.CONTENT,
regex: /^([^\[\r\n]+|\[)/
}
];
tokens.reverse();
strloop:
while (str.length) {
i = tokens.length;
while (i--) {
type = tokens[i].type;
// Check if the string matches any of the tokens
if (!(matches = str.match(tokens[i].regex)) ||
!matches[0]) {
continue;
}
// Add the match to the tokens list
toks.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) {
toks.push(tokenizeTag(TokenType.CONTENT, str));
}
str = '';
}
return toks;
};
/**
* Extracts the name an params from a tag
*
* @param {tokenType} type
* @param {string} val
* @return {Object}
* @private
*/
tokenizeTag = function (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 === TokenType.OPEN && (matches = val.match(openRegex))) {
name = lower(matches[1]);
if (matches[2] && (matches[2] = $.trim(matches[2]))) {
attrs = tokenizeAttrs(matches[2]);
}
}
if (type === TokenType.CLOSE && (matches = val.match(closeRegex))) {
name = lower(matches[1]);
}
if (type === TokenType.NEWLINE) {
name = '#newline';
}
// Treat all tokens without a name and
// all unknown BBCodes as content
if (!name ||
((type === TokenType.OPEN || type === TokenType.CLOSE) &&
!sceditorPlugins.bbcode.bbcodes[name])) {
type = TokenType.CONTENT;
name = '#';
}
return new TokenizeToken(type, name, val, attrs);
};
/**
* Extracts the individual attributes from a string containing
* all the attributes.
*
* @param {String} attrs
* @return {Array} Assoc array of attributes
* @private
*/
tokenizeAttrs = function (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 jQuery.sceditor.BBCodeParser.prototype
*/
base.parse = function (str, preserveNewLines) {
var ret = parseTokens(base.tokenize(str));
var opts = base.opts;
if (opts.fixInvalidChildren) {
fixChildren(ret);
}
if (opts.removeEmptyTags) {
removeEmpty(ret);
}
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 {tokenType} type
* @param {Array} arr
* @return {Boolean}
* @private
*/
hasTag = function (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
*/
isChildAllowed = function (parent, child) {
var parentBBCode = parent ? base.bbcodes[parent.name] : {},
allowedChildren = parentBBCode.allowedChildren;
if (base.opts.fixInvalidChildren && allowedChildren) {
return $.inArray(child.name || '#', allowedChildren) > -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
*/
parseTokens = function (toks) {
var token, bbcode, curTok, clone, i, previous, next,
cloned = [],
output = [],
openTags = [],
/**
* Returns the currently open tag or undefined
* @return {TokenizeToken}
*/
currentOpenTag = 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 (currentOpenTag()) {
currentOpenTag().children.push(token);
} else {
output.push(token);
}
},
/**
* Checks if this tag closes the current tag
* @param {String} name
* @return {Void}
*/
closesCurrentTag = function (name) {
return currentOpenTag() &&
(bbcode = base.bbcodes[currentOpenTag().name]) &&
bbcode.closedBy &&
$.inArray(name, bbcode.closedBy) > -1;
};
while ((token = toks.shift())) {
next = toks[0];
/* jshint indent:false */
switch (token.type) {
case TokenType.OPEN:
// Check it this closes a parent,
// e.g. for lists [*]one [*]two
if (closesCurrentTag(token.name)) {
openTags.pop();
}
addTag(token);
bbcode = base.bbcodes[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, TokenType.CLOSE, toks))) {
openTags.push(token);
} else if (!bbcode || !bbcode.isSelfClosing) {
token.type = TokenType.CONTENT;
}
break;
case TokenType.CLOSE:
// check if this closes the current tag,
// e.g. [/list] would close an open [*]
if (currentOpenTag() &&
token.name !== currentOpenTag().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 (currentOpenTag() &&
token.name === currentOpenTag().name) {
currentOpenTag().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, TokenType.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);
}
// 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 = TokenType.CONTENT;
addTag(token);
}
break;
case TokenType.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 (currentOpenTag() && next &&
closesCurrentTag(
(next.type === TokenType.CLOSE ? '/' : '') +
next.name
)) {
// skip if the next tag is the closing tag for
// the option tag, i.e. [/*]
if (!(next.type === TokenType.CLOSE &&
next.name === currentOpenTag().name)) {
bbcode = base.bbcodes[currentOpenTag().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;
}
previous = token;
}
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 {Bool} onlyRemoveBreakAfter
* @return {void}
*/
normaliseNewLines = function (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 = base.bbcodes[parent.name];
}
var i = childrenLength;
while (i--) {
if (!(token = children[i])) {
continue;
}
if (token.type === TokenType.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 === TokenType.OPEN) {
if ((bbcode = base.bbcodes[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 === TokenType.OPEN) {
if ((bbcode = base.bbcodes[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 === TokenType.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 {Array} [insideInline] Boolean, if inside an inline element
* @param {Array} [rootArr] Root array if there is one
* @return {Array}
* @private
*/
fixNesting = function (children, parents, insideInline, rootArr) {
var token, i, parent, parentIndex, parentParentChildren, right;
var isInline = function (token) {
var bbcode = base.bbcodes[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 !== TokenType.OPEN) {
continue;
}
if (!isInline(token) && insideInline) {
// 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;
parentIndex = $.inArray(parent, parentParentChildren);
if (parentIndex > -1) {
// remove the block level token from the right side of
// the split inline element
right.children.splice(
$.inArray(token, right.children), 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
);
// return to parents loop as the
// children have now increased
return;
}
}
parents.push(token);
fixNesting(
token.children,
parents,
insideInline || isInline(token),
rootArr
);
parents.pop(token);
}
};
/**
* 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 [b]only[/b] allows text.
* Instead of:
* Code only allows text.
*
* @param {Array} children
* @param {Array} [parent] Null if there is no parents
* @private
*/
fixChildren = function (children, parent) {
var token, args;
var i = children.length;
while (i--) {
if (!(token = children[i])) {
continue;
}
if (!isChildAllowed(parent, token)) {
// if it is not then convert it to text and see if it
// is allowed
token.name = null;
token.type = TokenType.CONTENT;
if (isChildAllowed(parent, token)) {
args = [i + 1, 0].concat(token.children);
if (token.closing) {
token.closing.name = null;
token.closing.type = TokenType.CONTENT;
args.push(token.closing);
}
i += args.length - 1;
Array.prototype.splice.apply(children, args);
} else {
parent.children.splice(i, 1);
}
}
if (token.type === TokenType.OPEN) {
fixChildren(token.children, token);
}
}
};
/**
* Removes any empty BBCodes which are not allowed to be empty.
*
* @param {Array} tokens
* @private
*/
removeEmpty = function (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 === TokenType.OPEN || type === TokenType.CLOSE) {
return false;
}
if (type === TokenType.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 !== TokenType.OPEN) {
continue;
}
bbcode = base.bbcodes[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,
$.merge([i, 1], token.children)
);
}
}
};
/**
* Converts a BBCode string to HTML
*
* @param {String} str
* @param {Bool} preserveNewLines If to preserve all new lines, not
* strip any based on the passed
* formatting options
* @return {String}
* @memberOf jQuery.sceditor.BBCodeParser.prototype
*/
base.toHTML = function (str, preserveNewLines) {
return convertToHTML(base.parse(str, preserveNewLines), true);
};
/**
* @private
*/
convertToHTML = function (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 === TokenType.OPEN) {
lastChild =
token.children[token.children.length - 1] || {};
bbcode = base.bbcodes[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(base.bbcodes[lastChild.name]) &&
!bbcode.isPreFormatted &&
!bbcode.skipLastLineBreak) {
// Add placeholder br to end of block level elements
// in all browsers apart from IE < 9 which handle
// new lines differently and doesn't need one.
if (!IE_BR_FIX) {
content += '
';
}
}
if (!$.isFunction(bbcode.html)) {
token.attrs['0'] = content;
html = sceditorPlugins.bbcode.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 === TokenType.NEWLINE) {
if (!isRoot) {
ret.push('
');
continue;
}
// If not already in a block wrap then start a new block
if (!blockWrapOpen) {
ret.push('
' + content + ''; } }, // END_COMMAND // START_COMMAND: Code code: { tags: { code: null }, isInline: false, allowedChildren: ['#', '#newline'], format: '[code]{0}[/code]', html: '
{0}
'
},
// END_COMMAND
// START_COMMAND: Left
left: {
styles: {
'text-align': [
'left',
'-webkit-left',
'-moz-left',
'-khtml-left'
]
},
isInline: false,
format: '[left]{0}[/left]',
html: '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|false} Returns false if name or bbcode is false * @since v1.3.5 */ set: function (name, bbcode) { if (!name || !bbcode) { return false; } // merge any existing command properties bbcode = $.extend(bbcodes[name] || {}, bbcode); bbcode.remove = function () { delete bbcodes[name]; }; bbcodes[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 v1.4.0 */ rename: function (name, newName) { if (name in bbcodes) { bbcodes[newName] = bbcodes[name]; delete bbcodes[name]; } else { return false; } return this; }, /** * Removes a BBCode * * @param {String} name * @return {this} * @since v1.3.5 */ remove: function (name) { if (name in bbcodes) { delete bbcodes[name]; } return this; } }; /** * Deprecated, use plugins: option instead. I.e.: * * $('textarea').sceditor({ * plugins: 'bbcode' * }); * * @deprecated */ $.fn.sceditorBBCodePlugin = function (options) { options = options || {}; if ($.isPlainObject(options)) { options.plugins = (options.plugins || '') + 'bbcode'; } return this.sceditor(options); }; /** * Converts CSS RGB and hex shorthand into hex * * @since v1.4.0 * @param {String} colorStr * @return {String} * @deprecated */ sceditorPlugins.bbcode.normaliseColour = _normaliseColour; sceditorPlugins.bbcode.formatString = _formatString; sceditorPlugins.bbcode.stripQuotes = _stripQuotes; sceditorPlugins.bbcode.bbcodes = bbcodes; SCEditor.BBCodeParser = BBCodeParser; })(jQuery, window, document);