2811 lines
64 KiB
JavaScript
2811 lines
64 KiB
JavaScript
/**
|
|
* 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>Code [b]only[/b] allows text.</code>
|
|
* Instead of:
|
|
* <code>Code <b>only</b> allows text.</code>
|
|
*
|
|
* @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 += '<br />';
|
|
}
|
|
}
|
|
|
|
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('<br />');
|
|
continue;
|
|
}
|
|
|
|
// If not already in a block wrap then start a new block
|
|
if (!blockWrapOpen) {
|
|
ret.push('<div>');
|
|
|
|
// If it's an empty DIV and compatibility mode is below
|
|
// IE8 then we must add a non-breaking space to the div
|
|
// otherwise the div will be collapsed. Adding a BR
|
|
// works but when you press enter to make a newline it
|
|
// suddenly goes back to the normal IE div behavior and
|
|
// creates two lines, one for the newline and one for
|
|
// the BR. I'm sure there must be a better fix but I've
|
|
// yet to find one.
|
|
// Cannot do zoom: 1; or set a height on the div to fix
|
|
// it as that causes resize handles to be added to the
|
|
// div when it's clicked on.
|
|
if (IE_VER < 8 || (document.documentMode &&
|
|
document.documentMode < 8)) {
|
|
ret.push('\u00a0');
|
|
}
|
|
}
|
|
|
|
// Putting BR in a div in IE causes it
|
|
// to do a double line break.
|
|
if (!IE_BR_FIX) {
|
|
ret.push('<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.push('<br />');
|
|
}
|
|
|
|
ret.push('</div>\n');
|
|
blockWrapOpen = false;
|
|
continue;
|
|
// content
|
|
} else {
|
|
needsBlockWrap = isRoot;
|
|
html = escapeEntities(token.val, true);
|
|
}
|
|
|
|
if (needsBlockWrap && !blockWrapOpen) {
|
|
ret.push('<div>');
|
|
blockWrapOpen = true;
|
|
} else if (!needsBlockWrap && blockWrapOpen) {
|
|
ret.push('</div>\n');
|
|
blockWrapOpen = false;
|
|
}
|
|
|
|
ret.push(html);
|
|
}
|
|
|
|
if (blockWrapOpen) {
|
|
ret.push('</div>\n');
|
|
}
|
|
|
|
return ret.join('');
|
|
};
|
|
|
|
/**
|
|
* 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 {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.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
|
|
*/
|
|
convertToBBCode = function (toks) {
|
|
var token, attr, bbcode, isBlock, isSelfClosing, quoteType,
|
|
breakBefore, breakStart, breakEnd, breakAfter,
|
|
// Create an array of strings which are joined together
|
|
// before being returned as this is faster in slow browsers.
|
|
// (Old versions of IE).
|
|
ret = [];
|
|
|
|
while (toks.length > 0) {
|
|
if (!(token = toks.shift())) {
|
|
continue;
|
|
}
|
|
// TODO: tidy this
|
|
bbcode = base.bbcodes[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 || BBCodeParser.QuoteType.auto;
|
|
|
|
if (!bbcode && token.type === TokenType.OPEN) {
|
|
ret.push(token.val);
|
|
|
|
if (token.children) {
|
|
ret.push(convertToBBCode(token.children));
|
|
}
|
|
|
|
if (token.closing) {
|
|
ret.push(token.closing.val);
|
|
}
|
|
} else if (token.type === TokenType.OPEN) {
|
|
if (breakBefore) {
|
|
ret.push('\n');
|
|
}
|
|
|
|
// Convert the tag and it's attributes to BBCode
|
|
ret.push('[' + token.name);
|
|
if (token.attrs) {
|
|
if (token.attrs.defaultattr) {
|
|
ret.push('=', quote(
|
|
token.attrs.defaultattr,
|
|
quoteType,
|
|
'defaultattr'
|
|
));
|
|
|
|
delete token.attrs.defaultattr;
|
|
}
|
|
|
|
for (attr in token.attrs) {
|
|
if (token.attrs.hasOwnProperty(attr)) {
|
|
ret.push(' ', attr, '=',
|
|
quote(token.attrs[attr], quoteType, attr));
|
|
}
|
|
}
|
|
}
|
|
ret.push(']');
|
|
|
|
if (breakStart) {
|
|
ret.push('\n');
|
|
}
|
|
|
|
// Convert the tags children to BBCode
|
|
if (token.children) {
|
|
ret.push(convertToBBCode(token.children));
|
|
}
|
|
|
|
// add closing tag if not self closing
|
|
if (!isSelfClosing && !bbcode.excludeClosing) {
|
|
if (breakEnd) {
|
|
ret.push('\n');
|
|
}
|
|
|
|
ret.push('[/' + token.name + ']');
|
|
}
|
|
|
|
if (breakAfter) {
|
|
ret.push('\n');
|
|
}
|
|
|
|
// preserve whatever was recognized as the
|
|
// closing tag if it is a self closing tag
|
|
if (token.closing && isSelfClosing) {
|
|
ret.push(token.closing.val);
|
|
}
|
|
} else {
|
|
ret.push(token.val);
|
|
}
|
|
}
|
|
|
|
return ret.join('');
|
|
};
|
|
|
|
/**
|
|
* Quotes an attribute
|
|
*
|
|
* @param {String} str
|
|
* @param {BBCodeParser.QuoteType} quoteType
|
|
* @param {String} name
|
|
* @return {String}
|
|
* @private
|
|
*/
|
|
quote = function (str, quoteType, name) {
|
|
var QuoteTypes = BBCodeParser.QuoteType,
|
|
needsQuotes = /\s|=/.test(str);
|
|
|
|
if ($.isFunction(quoteType)) {
|
|
return quoteType(str, name);
|
|
}
|
|
|
|
if (quoteType === QuoteTypes.never ||
|
|
(quoteType === QuoteTypes.auto && !needsQuotes)) {
|
|
return str;
|
|
}
|
|
|
|
return '"' + str.replace('\\', '\\\\').replace('"', '\\"') + '"';
|
|
};
|
|
|
|
/**
|
|
* Returns the last element of an array or null
|
|
*
|
|
* @param {Array} arr
|
|
* @return {Object} Last element
|
|
* @private
|
|
*/
|
|
last = function (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
|
|
*/
|
|
lower = function (str) {
|
|
return str.toLowerCase();
|
|
};
|
|
|
|
init();
|
|
};
|
|
|
|
/**
|
|
* Quote type
|
|
* @type {Object}
|
|
* @class QuoteType
|
|
* @name jQuery.sceditor.BBCodeParser.QuoteType
|
|
* @since v1.4.0
|
|
*/
|
|
BBCodeParser.QuoteType = {
|
|
/** @lends jQuery.sceditor.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
|
|
};
|
|
|
|
/**
|
|
* 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: BBCodeParser.QuoteType.auto
|
|
};
|
|
|
|
/**
|
|
* Deprecated, use sceditorPlugins.bbcode
|
|
*
|
|
* @class sceditorBBCodePlugin
|
|
* @name jQuery.sceditor.sceditorBBCodePlugin
|
|
* @deprecated
|
|
*/
|
|
$.sceditorBBCodePlugin =
|
|
/**
|
|
* BBCode plugin for SCEditor
|
|
*
|
|
* @class bbcode
|
|
* @name jQuery.sceditor.plugins.bbcode
|
|
* @since 1.4.1
|
|
*/
|
|
sceditorPlugins.bbcode = function () {
|
|
var base = this;
|
|
|
|
/**
|
|
* Private methods
|
|
* @private
|
|
*/
|
|
var buildBbcodeCache,
|
|
handleStyles,
|
|
handleTags,
|
|
removeFirstLastDiv;
|
|
|
|
base.bbcodes = sceditorPlugins.bbcode.bbcodes;
|
|
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 = {};
|
|
|
|
/**
|
|
* Same as tagsToBBCodes but instead of HTML tags it's styles
|
|
* @private
|
|
*/
|
|
var stylesToBBCodes = {};
|
|
|
|
/**
|
|
* 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']
|
|
};
|
|
|
|
/**
|
|
* Initializer
|
|
* @private
|
|
*/
|
|
base.init = function () {
|
|
base.opts = this.opts;
|
|
|
|
// build the BBCode cache
|
|
buildBbcodeCache();
|
|
|
|
this.commands = $.extend(
|
|
true, {}, defaultCommandsOverrides, this.commands
|
|
);
|
|
|
|
// Add BBCode helper methods
|
|
this.toBBCode = base.signalToSource;
|
|
this.fromBBCode = base.signalToWysiwyg;
|
|
};
|
|
|
|
/**
|
|
* Populates tagsToBBCodes and stylesToBBCodes to enable faster lookups
|
|
*
|
|
* @private
|
|
*/
|
|
buildBbcodeCache = function () {
|
|
$.each(base.bbcodes, function (bbcode) {
|
|
var isBlock,
|
|
tags = base.bbcodes[bbcode].tags,
|
|
styles = base.bbcodes[bbcode].styles;
|
|
|
|
if (tags) {
|
|
$.each(tags, function (tag, values) {
|
|
isBlock = base.bbcodes[bbcode].isInline === false;
|
|
|
|
tagsToBBCodes[tag] = tagsToBBCodes[tag] || {};
|
|
|
|
tagsToBBCodes[tag][isBlock] =
|
|
tagsToBBCodes[tag][isBlock] || {};
|
|
|
|
tagsToBBCodes[tag][isBlock][bbcode] = values;
|
|
});
|
|
}
|
|
|
|
if (styles) {
|
|
$.each(styles, function (style, values) {
|
|
isBlock = base.bbcodes[bbcode].isInline === false;
|
|
|
|
stylesToBBCodes[isBlock] =
|
|
stylesToBBCodes[isBlock] || {};
|
|
|
|
stylesToBBCodes[isBlock][style] =
|
|
stylesToBBCodes[isBlock][style] || {};
|
|
|
|
stylesToBBCodes[isBlock][style][bbcode] = values;
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Checks if any bbcode styles match the elements styles
|
|
*
|
|
* @return string Content with any matching
|
|
* bbcode tags wrapped around it.
|
|
* @private
|
|
*/
|
|
handleStyles = function ($element, content, blockLevel) {
|
|
var styleValue, format,
|
|
getStyle = SCEditor.dom.getStyle;
|
|
|
|
// convert blockLevel to boolean
|
|
blockLevel = !!blockLevel;
|
|
|
|
if (!stylesToBBCodes[blockLevel]) {
|
|
return content;
|
|
}
|
|
|
|
$.each(stylesToBBCodes[blockLevel], function (property, bbcodes) {
|
|
styleValue = getStyle($element[0], property);
|
|
|
|
// 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 (!styleValue ||
|
|
getStyle($element.parent()[0], property) === styleValue) {
|
|
return;
|
|
}
|
|
|
|
$.each(bbcodes, function (bbcode, values) {
|
|
if (!values ||
|
|
$.inArray(styleValue.toString(), values) > -1) {
|
|
format = base.bbcodes[bbcode].format;
|
|
|
|
if ($.isFunction(format)) {
|
|
content = format.call(base, $element, content);
|
|
} else {
|
|
content = _formatString(format, content);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
return content;
|
|
};
|
|
|
|
/**
|
|
* Handles a HTML tag and finds any matching bbcodes
|
|
*
|
|
* @param {jQuery} $element The element to convert
|
|
* @param {String} content The Tags text content
|
|
* @param {Bool} blockLevel If to convert block level tags
|
|
* @return {String} Content with any matching bbcode tags
|
|
* wrapped around it.
|
|
* @private
|
|
*/
|
|
handleTags = function ($element, content, blockLevel) {
|
|
var convertBBCode, format,
|
|
element = $element[0],
|
|
tag = element.nodeName.toLowerCase();
|
|
|
|
// convert blockLevel to boolean
|
|
blockLevel = !!blockLevel;
|
|
|
|
if (tagsToBBCodes[tag] && tagsToBBCodes[tag][blockLevel]) {
|
|
// loop all bbcodes for this tag
|
|
$.each(tagsToBBCodes[tag][blockLevel], function (
|
|
bbcode, bbcodeAttribs) {
|
|
// if the bbcode requires any attributes then check this has
|
|
// all needed
|
|
if (bbcodeAttribs) {
|
|
convertBBCode = false;
|
|
|
|
// loop all the bbcode attribs
|
|
$.each(bbcodeAttribs, function (attrib, values) {
|
|
// Skip if the element doesn't have the attibue or
|
|
// the attribute doesn't match one of the require
|
|
// values
|
|
if (!$element.attr(attrib) || (values &&
|
|
$.inArray($element.attr(attrib), values) < 0)) {
|
|
return;
|
|
}
|
|
|
|
// break this loop as we have matched this bbcode
|
|
convertBBCode = true;
|
|
return false;
|
|
});
|
|
|
|
if (!convertBBCode) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
format = base.bbcodes[bbcode].format;
|
|
|
|
if ($.isFunction(format)) {
|
|
content = format.call(base, $element, content);
|
|
} else {
|
|
content = _formatString(format, content);
|
|
}
|
|
});
|
|
}
|
|
|
|
var isInline = SCEditor.dom.isInline;
|
|
if (blockLevel && (!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 &&
|
|
!$(previousSibling).is('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
|
|
// except in IE < 11
|
|
// <block><inline><br></inline></block>
|
|
do {
|
|
parent = element.parentNode;
|
|
parentLastChild = 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)
|
|
// * Is IE < 11 and the tag is BR. IE < 11 never collapses BR
|
|
// tags.
|
|
if (!isLastBlockChild || tag === 'li' ||
|
|
(tag === 'br' && IE_BR_FIX)) {
|
|
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 &&
|
|
!$(previousSibling).is('br') &&
|
|
isInline(previousSibling, true)) {
|
|
content = '\n' + content;
|
|
}
|
|
}
|
|
|
|
return content;
|
|
};
|
|
|
|
/**
|
|
* Converts HTML to BBCode
|
|
*
|
|
* @param {String} html Html string, this function ignores this,
|
|
* it works off domBody
|
|
* @param {jQuery} $body Editors dom body object to convert
|
|
* @return {String} BBCode which has been converted from HTML
|
|
* @memberOf jQuery.plugins.bbcode.prototype
|
|
*/
|
|
base.signalToSource = function (html, $body) {
|
|
var $tmpContainer, bbcode,
|
|
parser = new BBCodeParser(base.opts.parserOptions);
|
|
|
|
if (!$body) {
|
|
if (typeof html === 'string') {
|
|
$tmpContainer = $('<div />')
|
|
.css('visibility', 'hidden')
|
|
.appendTo(document.body)
|
|
.html(html);
|
|
|
|
$body = $tmpContainer;
|
|
} else {
|
|
$body = $(html);
|
|
}
|
|
}
|
|
|
|
if (!$body || !$body.jquery) {
|
|
return '';
|
|
}
|
|
|
|
SCEditor.dom.removeWhiteSpace($body[0]);
|
|
|
|
// Remove all the stuff that is meant to be ignored
|
|
$('.sceditor-ignore', $body).remove();
|
|
|
|
bbcode = base.elementToBbcode($body);
|
|
|
|
if ($tmpContainer) {
|
|
$tmpContainer.remove();
|
|
}
|
|
|
|
bbcode = parser.toBBCode(bbcode, true);
|
|
|
|
if (base.opts.bbcodeTrim) {
|
|
bbcode = $.trim(bbcode);
|
|
}
|
|
|
|
return bbcode;
|
|
};
|
|
|
|
/**
|
|
* Converts a HTML dom element to BBCode starting from
|
|
* the innermost element and working backwards
|
|
*
|
|
* @private
|
|
* @param {jQuery} $element The element to convert to BBCode
|
|
* @return {string} BBCode
|
|
* @memberOf jQuery.plugins.bbcode.prototype
|
|
*/
|
|
base.elementToBbcode = function ($element) {
|
|
var toBBCode = function (node, vChildren) {
|
|
var ret = '';
|
|
// TODO: Move to BBCode class?
|
|
SCEditor.dom.traverse(node, function (node) {
|
|
var $node = $(node),
|
|
curTag = '',
|
|
nodeType = node.nodeType,
|
|
tag = node.nodeName.toLowerCase(),
|
|
vChild = validChildren[tag],
|
|
firstChild = node.firstChild,
|
|
isValidChild = true;
|
|
|
|
if (typeof vChildren === 'object') {
|
|
isValidChild = $.inArray(tag, vChildren) > -1;
|
|
|
|
// Emoticons should always be converted
|
|
if ($node.is('img') &&
|
|
$node.data('sceditor-emoticon')) {
|
|
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 ($node.hasClass('sceditor-nlf')) {
|
|
if (!firstChild || (!IE_BR_FIX &&
|
|
node.childNodes.length === 1 &&
|
|
/br/i.test(firstChild.nodeName))) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// don't loop inside iframes
|
|
if (tag !== 'iframe') {
|
|
curTag = 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') {
|
|
// handle inline bbcodes
|
|
curTag = handleStyles($node, curTag);
|
|
curTag = handleTags($node, curTag);
|
|
|
|
// handle blocklevel bbcodes
|
|
curTag = handleStyles($node, curTag, true);
|
|
}
|
|
|
|
ret += handleTags($node, curTag, true);
|
|
} else {
|
|
ret += curTag;
|
|
}
|
|
} else {
|
|
ret += node.nodeValue;
|
|
}
|
|
}, false, true);
|
|
|
|
return ret;
|
|
};
|
|
|
|
return toBBCode($element[0]);
|
|
};
|
|
|
|
/**
|
|
* Converts BBCode to HTML
|
|
*
|
|
* @param {String} text
|
|
* @param {Bool} asFragment
|
|
* @return {String} HTML
|
|
* @memberOf jQuery.plugins.bbcode.prototype
|
|
*/
|
|
base.signalToWysiwyg = function (text, asFragment) {
|
|
var parser = new BBCodeParser(base.opts.parserOptions),
|
|
html = parser.toHTML(base.opts.bbcodeTrim ?
|
|
$.trim(text) : text);
|
|
|
|
return asFragment ? removeFirstLastDiv(html) : html;
|
|
};
|
|
|
|
/**
|
|
* Removes the first and last divs from the HTML.
|
|
*
|
|
* This is needed for pasting
|
|
* @param {String} html
|
|
* @return {String}
|
|
* @private
|
|
*/
|
|
removeFirstLastDiv = function (html) {
|
|
var node, next, removeDiv,
|
|
$output = $('<div />').hide().appendTo(document.body),
|
|
output = $output[0];
|
|
|
|
removeDiv = function (node, isFirst) {
|
|
// Don't remove divs that have styling
|
|
if (SCEditor.dom.hasStyling(node)) {
|
|
return;
|
|
}
|
|
|
|
if (IE_BR_FIX || (node.childNodes.length !== 1 ||
|
|
!$(node.firstChild).is('br'))) {
|
|
while ((next = node.firstChild)) {
|
|
output.insertBefore(next, node);
|
|
}
|
|
}
|
|
|
|
if (isFirst) {
|
|
var lastChild = output.lastChild;
|
|
|
|
if (node !== lastChild && $(lastChild).is('div') &&
|
|
node.nextSibling === lastChild) {
|
|
output.insertBefore(document.createElement('br'), node);
|
|
}
|
|
}
|
|
|
|
output.removeChild(node);
|
|
};
|
|
|
|
output.innerHTML = html.replace(/<\/div>\n/g, '</div>');
|
|
|
|
if ((node = output.firstChild) && $(node).is('div')) {
|
|
removeDiv(node, true);
|
|
}
|
|
|
|
if ((node = output.lastChild) && $(node).is('div')) {
|
|
removeDiv(node);
|
|
}
|
|
|
|
output = output.innerHTML;
|
|
$output.remove();
|
|
|
|
return output;
|
|
};
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* 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 1.4.5
|
|
*/
|
|
sceditorPlugins.bbcode.formatBBCodeString = function (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];
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Converts a number 0-255 to hex.
|
|
*
|
|
* Will return 00 if number is not a valid number.
|
|
*
|
|
* @param {Number} number
|
|
* @return {String}
|
|
* @private
|
|
*/
|
|
var toHex = function (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;
|
|
};
|
|
|
|
var _normaliseColour = function (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] - 0) +
|
|
toHex(match[3] - 0);
|
|
}
|
|
|
|
// 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;
|
|
};
|
|
|
|
var bbcodes = {
|
|
// 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: BBCodeParser.QuoteType.never,
|
|
format: function ($element, content) {
|
|
var font;
|
|
|
|
if (!$element.is('font') || !(font = $element.attr('face'))) {
|
|
font = $element.css('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 = element.attr('size'),
|
|
size = 2;
|
|
|
|
if (!fontSize) {
|
|
fontSize = element.css('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: BBCodeParser.QuoteType.never,
|
|
format: function ($element, content) {
|
|
var color;
|
|
|
|
if (!$element.is('font') || !(color = $element.attr('color'))) {
|
|
color = $element[0].style.color || $element.css('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 ($elm, content) {
|
|
return $elm.data('sceditor-emoticon') + 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: BBCodeParser.QuoteType.never,
|
|
format: function ($element, content) {
|
|
var width, height,
|
|
attribs = '',
|
|
element = $element[0],
|
|
style = function (name) {
|
|
return element.style ? element.style[name] : null;
|
|
};
|
|
|
|
// check if this is an emoticon image
|
|
if ($element.attr('data-sceditor-emoticon')) {
|
|
return content;
|
|
}
|
|
|
|
width = $element.attr('width') || style('width');
|
|
height = $element.attr('height') || style('height');
|
|
|
|
// only add width and height if one is specified
|
|
if ((element.complete && (width || height)) ||
|
|
(width && height)) {
|
|
attribs = '=' + $element.width() + 'x' + $element.height();
|
|
}
|
|
|
|
return '[img' + attribs + ']' + $element.attr('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: BBCodeParser.QuoteType.never,
|
|
format: function (element, content) {
|
|
var url = element.attr('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: BBCodeParser.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: BBCodeParser.QuoteType.never,
|
|
format: function (element, content) {
|
|
var author = '';
|
|
var $elm = $(element);
|
|
var $cite = $elm.children('cite').first();
|
|
|
|
if ($cite.length === 1 || $elm.data('author')) {
|
|
author = $cite.text() || $elm.data('author');
|
|
|
|
$elm.data('author', author);
|
|
$cite.remove();
|
|
|
|
content = this.elementToBbcode($(element));
|
|
author = '=' + author.replace(/(^\s+|\s+$)/g, '');
|
|
|
|
$elm.prepend($cite);
|
|
}
|
|
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 = element.attr('data-youtube-id');
|
|
|
|
return element ? '[youtube]' + element + '[/youtube]' : content;
|
|
},
|
|
html: '<iframe width="560" height="315" frameborder="0" ' +
|
|
'src="https://www.youtube.com/embed/{0}?wmode=opaque" ' +
|
|
'data-youtube-id="{0}" allowfullscreen></iframe>'
|
|
},
|
|
// END_COMMAND
|
|
|
|
|
|
// START_COMMAND: Rtl
|
|
rtl: {
|
|
styles: {
|
|
direction: ['rtl']
|
|
},
|
|
format: '[rtl]{0}[/rtl]',
|
|
html: '<div style="direction: rtl">{0}</div>'
|
|
},
|
|
// END_COMMAND
|
|
|
|
// START_COMMAND: Ltr
|
|
ltr: {
|
|
styles: {
|
|
direction: ['ltr']
|
|
},
|
|
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: {}
|
|
};
|
|
|
|
/**
|
|
* Static BBCode helper class
|
|
* @class command
|
|
* @name jQuery.plugins.bbcode.bbcode
|
|
*/
|
|
sceditorPlugins.bbcode.bbcode =
|
|
/** @lends jQuery.plugins.bbcode.bbcode */
|
|
{
|
|
/**
|
|
* Gets a BBCode
|
|
*
|
|
* @param {String} name
|
|
* @return {Object|null}
|
|
* @since v1.3.5
|
|
*/
|
|
get: function (name) {
|
|
return bbcodes[name] || null;
|
|
},
|
|
|
|
/**
|
|
* <p>Adds a BBCode to the parser or updates an existing
|
|
* BBCode if a BBCode with the specified name already exists.</p>
|
|
*
|
|
* @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);
|