2
0

jquery.sceditor.bbcode.js 62 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453
  1. /**
  2. * SCEditor BBCode Plugin
  3. * http://www.sceditor.com/
  4. *
  5. * Copyright (C) 2011-2013, Sam Clarke (samclarke.com)
  6. *
  7. * SCEditor is licensed under the MIT license:
  8. * http://www.opensource.org/licenses/mit-license.php
  9. *
  10. * @fileoverview SCEditor BBCode Plugin
  11. * @author Sam Clarke
  12. * @requires jQuery
  13. */
  14. // ==ClosureCompiler==
  15. // @output_file_name bbcode.min.js
  16. // @compilation_level SIMPLE_OPTIMIZATIONS
  17. // ==/ClosureCompiler==
  18. /*jshint smarttabs: true, jquery: true, eqnull:true, curly: false */
  19. /*global prompt: true*/
  20. (function($, window, document) {
  21. 'use strict';
  22. /**
  23. * SCEditor BBCode parser class
  24. *
  25. * @param {Object} options
  26. * @class BBCodeParser
  27. * @name jQuery.sceditor.BBCodeParser
  28. * @since v1.4.0
  29. */
  30. $.sceditor.BBCodeParser = function(options) {
  31. // make sure this is not being called as a function
  32. if(!(this instanceof $.sceditor.BBCodeParser))
  33. return new $.sceditor.BBCodeParser(options);
  34. var base = this;
  35. // Private methods
  36. var init,
  37. tokenizeTag,
  38. tokenizeAttrs,
  39. parseTokens,
  40. normaliseNewLines,
  41. fixNesting,
  42. isChildAllowed,
  43. removeEmpty,
  44. fixChildren,
  45. convertToHTML,
  46. convertToBBCode,
  47. hasTag,
  48. quote,
  49. lower,
  50. last;
  51. /**
  52. * Enum of valid token types
  53. * @type {Object}
  54. * @private
  55. */
  56. var tokenType = {
  57. open: 'open',
  58. content: 'content',
  59. newline: 'newline',
  60. close: 'close'
  61. };
  62. /**
  63. * Tokenize token class
  64. *
  65. * @param {String} type The type of token this is, should be one of tokenType
  66. * @param {String} name The name of this token
  67. * @param {String} val The originally matched string
  68. * @param {Array} attrs Any attributes. Only set on tokenType.open tokens
  69. * @param {Array} children Any children of this token
  70. * @param {TokenizeToken} closing This tokens closing tag. Only set on tokenType.open tokens
  71. * @class TokenizeToken
  72. * @name TokenizeToken
  73. * @memberOf jQuery.sceditor.BBCodeParser.prototype
  74. */
  75. var TokenizeToken = function(type, name, val, attrs, children, closing) {
  76. var base = this;
  77. base.type = type;
  78. base.name = name;
  79. base.val = val;
  80. base.attrs = attrs || {};
  81. base.children = children || [];
  82. base.closing = closing || null;
  83. };
  84. // Declaring methods via prototype instead of in the constructor
  85. // to reduce memory usage as there could be a lot or these
  86. // objects created.
  87. TokenizeToken.prototype = {
  88. /** @lends jQuery.sceditor.BBCodeParser.prototype.TokenizeToken */
  89. /**
  90. * Clones this token
  91. * @param {Bool} includeChildren If to include the children in the clone. Defaults to false.
  92. * @return {TokenizeToken}
  93. */
  94. clone: function(includeChildren) {
  95. var base = this;
  96. return new TokenizeToken(
  97. base.type,
  98. base.name,
  99. base.val,
  100. base.attrs,
  101. includeChildren ? base.children : [],
  102. base.closing ? base.closing.clone() : null
  103. );
  104. },
  105. /**
  106. * Splits this token at the specified child
  107. * @param {TokenizeToken|Int} splitAt The child to split at or the index of the child
  108. * @return {TokenizeToken} The right half of the split token or null if failed
  109. */
  110. splitAt: function(splitAt) {
  111. var clone,
  112. base = this,
  113. splitAtLength = 0,
  114. childrenLen = base.children.length;
  115. if(typeof object !== 'number')
  116. splitAt = $.inArray(splitAt, base.children);
  117. if(splitAt < 0 || splitAt > childrenLen)
  118. return null;
  119. // Work out how many items are on the right side of the split
  120. // to pass to splice()
  121. while(childrenLen--)
  122. {
  123. if(childrenLen >= splitAt)
  124. splitAtLength++;
  125. else
  126. childrenLen = 0;
  127. }
  128. clone = base.clone();
  129. clone.children = base.children.splice(splitAt, splitAtLength);
  130. return clone;
  131. }
  132. };
  133. init = function() {
  134. base.opts = $.extend({}, $.sceditor.BBCodeParser.defaults, options);
  135. base.bbcodes = $.sceditor.plugins.bbcode.bbcodes;
  136. };
  137. /**
  138. * Takes a BBCode string and splits it into open, content and close tags.
  139. *
  140. * It does no checking to verify a tag has a matching open or closing tag
  141. * or if the tag is valid child of any tag before it. For that the tokens
  142. * should be passed to the parse function.
  143. *
  144. * @param {String} str
  145. * @return {Array}
  146. * @memberOf jQuery.sceditor.BBCodeParser.prototype
  147. */
  148. base.tokenize = function(str) {
  149. var matches, type, i,
  150. toks = [],
  151. tokens = [
  152. // Close must come before open as they are
  153. // the same except close has a / at the start.
  154. {
  155. type: 'close',
  156. regex: /^\[\/[^\[\]]+\]/
  157. },
  158. {
  159. type: 'open',
  160. regex: /^\[[^\[\]]+\]/
  161. },
  162. {
  163. type: 'newline',
  164. regex: /^(\r\n|\r|\n)/
  165. },
  166. {
  167. type: 'content',
  168. regex: /^([^\[\r\n]+|\[)/
  169. }
  170. ];
  171. tokens.reverse();
  172. strloop:
  173. while(str.length)
  174. {
  175. i = tokens.length;
  176. while(i--)
  177. {
  178. type = tokens[i].type;
  179. // Check if the string matches any of the tokens
  180. if(!(matches = str.match(tokens[i].regex)) || !matches[0])
  181. continue;
  182. // Add the match to the tokens list
  183. toks.push(tokenizeTag(type, matches[0]));
  184. // Remove the match from the string
  185. str = str.substr(matches[0].length);
  186. // The token has been added so start again
  187. continue strloop;
  188. }
  189. // If there is anything left in the string which doesn't match
  190. // any of the tokens then just assume it's content and add it.
  191. if(str.length)
  192. toks.push(tokenizeTag(tokenType.content, str));
  193. str = '';
  194. }
  195. return toks;
  196. };
  197. /**
  198. * Extracts the name an params from a tag
  199. *
  200. * @param {Object} token
  201. * @return {Object}
  202. * @private
  203. */
  204. tokenizeTag = function(type, val) {
  205. var matches, attrs, name;
  206. // Extract the name and attributes from opening tags and
  207. // just the name from closing tags.
  208. if(type === 'open' && (matches = val.match(/\[([^\]\s=]+)(?:([^\]]+))?\]/)))
  209. {
  210. name = lower(matches[1]);
  211. if(matches[2] && (matches[2] = $.trim(matches[2])))
  212. attrs = tokenizeAttrs(matches[2]);
  213. }
  214. else if(type === 'close' && (matches = val.match(/\[\/([^\[\]]+)\]/)))
  215. name = lower(matches[1]);
  216. else if(type === 'newline')
  217. name = '#newline';
  218. // Treat all tokens without a name and all unknown BBCodes as content
  219. if(!name || (type === 'open' || type === 'close') && !$.sceditor.plugins.bbcode.bbcodes[name])
  220. {
  221. type = 'content';
  222. name = '#';
  223. }
  224. return new TokenizeToken(type, name, val, attrs);
  225. };
  226. /**
  227. * Extracts the individual attributes from a string containing
  228. * all the attributes.
  229. *
  230. * @param {String} attrs
  231. * @return {Array} Assoc array of attributes
  232. * @private
  233. */
  234. tokenizeAttrs = function(attrs) {
  235. var matches,
  236. /*
  237. ([^\s=]+) Anything that's not a space or equals
  238. = Equals =
  239. (?:
  240. (?:
  241. (["']) The opening quote
  242. (
  243. (?:\\\2|[^\2])*? Anything that isn't the unescaped opening quote
  244. )
  245. \2 The opening quote again which will now close the string
  246. )
  247. | If not a quoted string then match
  248. (
  249. (?:.(?!\s\S+=))*.? Anything that isn't part of [space][non-space][=] which would be a new attribute
  250. )
  251. )
  252. */
  253. atribsRegex = /([^\s=]+)=(?:(?:(["'])((?:\\\2|[^\2])*?)\2)|((?:.(?!\s\S+=))*.))/g,
  254. unquote = $.sceditor.plugins.bbcode.stripQuotes,
  255. ret = {};
  256. // if only one attribute then remove the = from the start and strip any quotes
  257. if(attrs.charAt(0) === '=' && attrs.indexOf('=', 1) < 0)
  258. ret.defaultattr = unquote(attrs.substr(1));
  259. else
  260. {
  261. if(attrs.charAt(0) === '=')
  262. attrs = 'defaultattr' + attrs;
  263. // No need to strip quotes here, the regex will do that.
  264. while((matches = atribsRegex.exec(attrs)))
  265. ret[lower(matches[1])] = unquote(matches[3]) || matches[4];
  266. }
  267. return ret;
  268. };
  269. /**
  270. * Parses a string into an array of BBCodes
  271. *
  272. * @param {String} str
  273. * @param {Bool} preserveNewLines If to preserve all new lines, not strip any based on the passed formatting options
  274. * @return {Array} Array of BBCode objects
  275. * @memberOf jQuery.sceditor.BBCodeParser.prototype
  276. */
  277. base.parse = function(str, preserveNewLines) {
  278. var ret = parseTokens(base.tokenize(str));
  279. if(base.opts.fixInvalidChildren)
  280. fixChildren(ret);
  281. if(base.opts.removeEmptyTags)
  282. removeEmpty(ret);
  283. if(base.opts.fixInvalidNesting)
  284. fixNesting(ret);
  285. normaliseNewLines(ret, null, preserveNewLines);
  286. if(base.opts.removeEmptyTags)
  287. removeEmpty(ret);
  288. return ret;
  289. };
  290. /**
  291. * Checks if an array of TokenizeToken's contains the
  292. * specified token.
  293. *
  294. * Checks the tokens name and type match another tokens
  295. * name and type in the array.
  296. *
  297. * @param {string} name
  298. * @param {tokenType} type
  299. * @param {Array} arr
  300. * @return {Boolean}
  301. * @private
  302. */
  303. hasTag = function(name, type, arr) {
  304. var i = arr.length;
  305. while(i--)
  306. if(arr[i].type === type && arr[i].name === name)
  307. return true;
  308. return false;
  309. };
  310. /**
  311. * Checks if the child tag is allowed as one
  312. * of the parent tags children.
  313. *
  314. * @param {TokenizeToken} parent
  315. * @param {TokenizeToken} child
  316. * @return {Boolean}
  317. * @private
  318. */
  319. isChildAllowed = function(parent, child) {
  320. var bbcode = parent ? base.bbcodes[parent.name] : null,
  321. allowedChildren = bbcode ? bbcode.allowedChildren : null;
  322. if(!base.opts.fixInvalidChildren || !allowedChildren)
  323. return true;
  324. if(allowedChildren && $.inArray(child.name || '#', allowedChildren) < 0)
  325. return false;
  326. return true;
  327. };
  328. // TODO: Tidy this parseTokens() function up a bit.
  329. /**
  330. * Parses an array of tokens created by tokenize()
  331. *
  332. * @param {Array} toks
  333. * @return {Array} Parsed tokens
  334. * @see tokenize()
  335. * @private
  336. */
  337. parseTokens = function(toks) {
  338. var token, bbcode, curTok, clone, i, previous, next,
  339. cloned = [],
  340. output = [],
  341. openTags = [],
  342. /**
  343. * Returns the currently open tag or undefined
  344. * @return {TokenizeToken}
  345. */
  346. currentOpenTag = function() {
  347. return last(openTags);
  348. },
  349. /**
  350. * Adds a tag to either the current tags children
  351. * or to the output array.
  352. * @param {TokenizeToken} token
  353. * @private
  354. */
  355. addTag = function(token) {
  356. if(currentOpenTag())
  357. currentOpenTag().children.push(token);
  358. else
  359. output.push(token);
  360. },
  361. /**
  362. * Checks if this tag closes the current tag
  363. * @param {String} name
  364. * @return {Void}
  365. */
  366. closesCurrentTag = function(name) {
  367. return currentOpenTag() &&
  368. (bbcode = base.bbcodes[currentOpenTag().name]) &&
  369. bbcode.closedBy &&
  370. $.inArray(name, bbcode.closedBy) > -1;
  371. };
  372. while((token = toks.shift()))
  373. {
  374. next = toks[0];
  375. switch(token.type)
  376. {
  377. case tokenType.open:
  378. // Check it this closes a parent, i.e. for lists [*]one [*]two
  379. if(closesCurrentTag(token.name))
  380. openTags.pop();
  381. addTag(token);
  382. bbcode = base.bbcodes[token.name];
  383. // If this tag is not self closing and it has a closing tag then it is open and has children so
  384. // add it to the list of open tags. If has the closedBy property then it is closed by other tags
  385. // so include everything as it's children until one of those tags is reached.
  386. if((!bbcode || !bbcode.isSelfClosing) && (bbcode.closedBy || hasTag(token.name, tokenType.close, toks)))
  387. openTags.push(token);
  388. else if(!bbcode || !bbcode.isSelfClosing)
  389. token.type = tokenType.content;
  390. break;
  391. case tokenType.close:
  392. // check if this closes the current tag, e.g. [/list] would close an open [*]
  393. if(currentOpenTag() && token.name !== currentOpenTag().name && closesCurrentTag('/' + token.name))
  394. openTags.pop();
  395. // If this is closing the currently open tag just pop the close
  396. // tag off the open tags array
  397. if(currentOpenTag() && token.name === currentOpenTag().name)
  398. {
  399. currentOpenTag().closing = token;
  400. openTags.pop();
  401. }
  402. // If this is closing an open tag that is the parent of the current
  403. // tag then clone all the tags including the current one until
  404. // reaching the parent that is being closed. Close the parent and then
  405. // add the clones back in.
  406. else if(hasTag(token.name, tokenType.open, openTags))
  407. {
  408. // Remove the tag from the open tags
  409. while((curTok = openTags.pop()))
  410. {
  411. // If it's the tag that is being closed then
  412. // discard it and break the loop.
  413. if(curTok.name === token.name)
  414. {
  415. curTok.closing = token;
  416. break;
  417. }
  418. // Otherwise clone this tag and then add any
  419. // previously cloned tags as it's children
  420. clone = curTok.clone();
  421. if(cloned.length > 1)
  422. clone.children.push(last(cloned));
  423. cloned.push(clone);
  424. }
  425. // Add the last cloned child to the now current tag
  426. // (the parent of the tag which was being closed)
  427. addTag(last(cloned));
  428. // Add all the cloned tags to the open tags list
  429. i = cloned.length;
  430. while(i--)
  431. openTags.push(cloned[i]);
  432. cloned.length = 0;
  433. }
  434. // This tag is closing nothing so treat it as content
  435. else
  436. {
  437. token.type = tokenType.content;
  438. addTag(token);
  439. }
  440. break;
  441. case tokenType.newline:
  442. // handle things like
  443. // [*]list\nitem\n[*]list1
  444. // where it should come out as
  445. // [*]list\nitem[/*]\n[*]list1[/*]
  446. // instead of
  447. // [*]list\nitem\n[/*][*]list1[/*]
  448. if(currentOpenTag() && next && closesCurrentTag((next.type === tokenType.close ? '/' : '') + next.name))
  449. {
  450. // skip if the next tag is the closing tag for the option tag, i.e. [/*]
  451. if(!(next.type === tokenType.close && next.name === currentOpenTag().name))
  452. {
  453. bbcode = base.bbcodes[currentOpenTag().name];
  454. if(bbcode && bbcode.breakAfter)
  455. openTags.pop();
  456. else if(bbcode && bbcode.isInline === false && base.opts.breakAfterBlock && bbcode.breakAfter !== false)
  457. openTags.pop();
  458. }
  459. }
  460. addTag(token);
  461. break;
  462. default: // content
  463. addTag(token);
  464. break;
  465. }
  466. previous = token;
  467. }
  468. return output;
  469. };
  470. /**
  471. * Normalise all new lines
  472. *
  473. * Removes any formatting new lines from the BBCode
  474. * leaving only content ones. I.e. for a list:
  475. *
  476. * [list]
  477. * [*] list item one
  478. * with a line break
  479. * [*] list item two
  480. * [/list]
  481. *
  482. * would become
  483. *
  484. * [list] [*] list item one
  485. * with a line break [*] list item two [/list]
  486. *
  487. * Which makes it easier to convert to HTML or add
  488. * the formatting new lines back in when converting
  489. * back to BBCode
  490. *
  491. * @param {Array} children
  492. * @param {TokenizeToken} parent
  493. * @param {Bool} onlyRemoveBreakAfter
  494. * @return {void}
  495. */
  496. normaliseNewLines = function(children, parent, onlyRemoveBreakAfter) {
  497. var token, left, right, parentBBCode, bbcode,
  498. removedBreakEnd, removedBreakBefore, remove,
  499. childrenLength = children.length,
  500. i = childrenLength;
  501. if(parent)
  502. parentBBCode = base.bbcodes[parent.name];
  503. while(i--)
  504. {
  505. if(!(token = children[i]))
  506. continue;
  507. if(token.type === tokenType.newline)
  508. {
  509. left = i > 0 ? children[i - 1] : null;
  510. right = i < childrenLength - 1 ? children[i+1] : null;
  511. remove = false;
  512. // Handle the start and end new lines e.g. [tag]\n and \n[/tag]
  513. if(!onlyRemoveBreakAfter && parentBBCode && parentBBCode.isSelfClosing !== true)
  514. {
  515. // First child of parent so must be opening line break (breakStartBlock, breakStart) e.g. [tag]\n
  516. if(!left)
  517. {
  518. if(parentBBCode.isInline === false && base.opts.breakStartBlock && parentBBCode.breakStart !== false)
  519. remove = true;
  520. if(parentBBCode.breakStart)
  521. remove = true;
  522. }
  523. // Last child of parent so must be end line break (breakEndBlock, breakEnd) e.g. \n[/tag]
  524. // remove last line break (breakEndBlock, breakEnd)
  525. else if (!removedBreakEnd && !right)
  526. {
  527. if(parentBBCode.isInline === false && base.opts.breakEndBlock && parentBBCode.breakEnd !== false)
  528. remove = true;
  529. if(parentBBCode.breakEnd)
  530. remove = true;
  531. removedBreakEnd = remove;
  532. }
  533. }
  534. if(left && left.type === tokenType.open)
  535. {
  536. if((bbcode = base.bbcodes[left.name]))
  537. {
  538. if(!onlyRemoveBreakAfter)
  539. {
  540. if(bbcode.isInline === false && base.opts.breakAfterBlock && bbcode.breakAfter !== false)
  541. remove = true;
  542. if(bbcode.breakAfter)
  543. remove = true;
  544. }
  545. else if(bbcode.isInline === false)
  546. remove = true;
  547. }
  548. }
  549. if(!onlyRemoveBreakAfter && !removedBreakBefore && right && right.type === tokenType.open)
  550. {
  551. if((bbcode = base.bbcodes[right.name]))
  552. {
  553. if(bbcode.isInline === false && base.opts.breakBeforeBlock && bbcode.breakBefore !== false)
  554. remove = true;
  555. if(bbcode.breakBefore)
  556. remove = true;
  557. removedBreakBefore = remove;
  558. if(remove)
  559. {
  560. children.splice(i, 1);
  561. continue;
  562. }
  563. }
  564. }
  565. if(remove)
  566. children.splice(i, 1);
  567. // reset double removedBreakBefore removal protection.
  568. // This is needed for cases like \n\n[\tag] where
  569. // only 1 \n should be removed but without this they both
  570. // would be.
  571. removedBreakBefore = false;
  572. }
  573. else if(token.type === tokenType.open)
  574. normaliseNewLines(token.children, token, onlyRemoveBreakAfter);
  575. }
  576. };
  577. /**
  578. * Fixes any invalid nesting.
  579. *
  580. * If it is a block level element inside 1 or more inline elements
  581. * then those inline elements will be split at the point where the
  582. * block level is and the block level element placed between the split
  583. * parts. i.e.
  584. * [inline]textA[blocklevel]textB[/blocklevel]textC[/inline]
  585. * Will become:
  586. * [inline]textA[/inline][blocklevel]textB[/blocklevel][inline]textC[/inline]
  587. *
  588. * @param {Array} children
  589. * @param {Array} [parents] Null if there is no parents
  590. * @param {Array} [insideInline] Boolean, if inside an inline element
  591. * @param {Array} [rootArr] Root array if there is one
  592. * @return {Array}
  593. * @private
  594. */
  595. fixNesting = function(children, parents, insideInline, rootArr) {
  596. var token, i, parent, parentIndex, parentParentChildren, right,
  597. isInline = function(token) {
  598. var bbcode = base.bbcodes[token.name];
  599. return !bbcode || bbcode.isInline !== false;
  600. };
  601. parents = parents || [];
  602. rootArr = rootArr || children;
  603. // this must check length each time as the length
  604. // can change as tokens are moved around to fix the nesting.
  605. for(i=0; i<children.length; i++)
  606. {
  607. if(!(token = children[i]) || token.type !== tokenType.open)
  608. continue;
  609. if(!isInline(token) && insideInline)
  610. {
  611. // if this is a blocklevel element inside an inline one then split
  612. // the parent at the block level element
  613. parent = last(parents);
  614. right = parent.splitAt(token);
  615. parentParentChildren = parents.length > 1 ? parents[parents.length - 2].children : rootArr;
  616. if((parentIndex = $.inArray(parent, parentParentChildren)) > -1)
  617. {
  618. // remove the block level token from the right side of the split
  619. // inline element
  620. right.children.splice($.inArray(token, right.children), 1);
  621. // insert the block level token and the right side after the left
  622. // side of the inline token
  623. parentParentChildren.splice(parentIndex+1, 0, token, right);
  624. // return to parents loop as the children have now increased
  625. return;
  626. }
  627. }
  628. parents.push(token);
  629. fixNesting(token.children, parents, insideInline || isInline(token), rootArr);
  630. parents.pop(token);
  631. }
  632. };
  633. /**
  634. * Fixes any invalid children.
  635. *
  636. * If it is an element which isn't allowed as a child of it's parent
  637. * then it will be converted to content of the parent element. i.e.
  638. * [code]Code [b]only[/b] allows text.[/code]
  639. * Will become:
  640. * <code>Code [b]only[/b] allows text.</code>
  641. * Instead of:
  642. * <code>Code <b>only</b> allows text.</code>
  643. *
  644. * @param {Array} children
  645. * @param {Array} [parent] Null if there is no parents
  646. * @private
  647. */
  648. fixChildren = function(children, parent) {
  649. var token, args,
  650. i = children.length;
  651. while(i--)
  652. {
  653. if(!(token = children[i]))
  654. continue;
  655. if(!isChildAllowed(parent, token))
  656. {
  657. // if it is not then convert it to text and see if it
  658. // is allowed
  659. token.name = null;
  660. token.type = tokenType.content;
  661. if(isChildAllowed(parent, token))
  662. {
  663. args = [i+1, 0].concat(token.children);
  664. if(token.closing)
  665. {
  666. token.closing.name = null;
  667. token.closing.type = tokenType.content;
  668. args.push(token.closing);
  669. }
  670. i += args.length - 1;
  671. Array.prototype.splice.apply(children, args);
  672. }
  673. else
  674. parent.children.splice(i, 1);
  675. }
  676. if(token.type === tokenType.open)
  677. fixChildren(token.children, token);
  678. }
  679. };
  680. /**
  681. * Removes any empty BBCodes which are not allowed to be empty.
  682. *
  683. * @param {Array} tokens
  684. * @private
  685. */
  686. removeEmpty = function(tokens) {
  687. var token, bbcode, isTokenWhiteSpace,
  688. i = tokens.length;
  689. /**
  690. * Checks if all children are whitespace or not
  691. * @private
  692. */
  693. isTokenWhiteSpace = function(children) {
  694. var j = children.length;
  695. while(j--)
  696. {
  697. if(children[j].type === tokenType.open)
  698. return false;
  699. if(children[j].type === tokenType.close)
  700. return false;
  701. if(children[j].type === tokenType.content && children[j].val && /\S|\u00A0/.test(children[j].val))
  702. return false;
  703. }
  704. return true;
  705. };
  706. while(i--)
  707. {
  708. // only tags can be empty, content can't be empty. So skip anything that isn't a tag.
  709. if(!(token = tokens[i]) || token.type !== tokenType.open)
  710. continue;
  711. bbcode = base.bbcodes[token.name];
  712. // remove any empty children of this tag first so that if they are all
  713. // removed this one doesn't think it's not empty.
  714. removeEmpty(token.children);
  715. if(isTokenWhiteSpace(token.children) && bbcode && !bbcode.isSelfClosing && !bbcode.allowsEmpty)
  716. tokens.splice.apply(tokens, $.merge([i, 1], token.children));
  717. }
  718. };
  719. /**
  720. * Converts a BBCode string to HTML
  721. * @param {String} str
  722. * @param {Bool} preserveNewLines If to preserve all new lines, not strip any based on the passed formatting options
  723. * @return {String}
  724. * @memberOf jQuery.sceditor.BBCodeParser.prototype
  725. */
  726. base.toHTML = function(str, preserveNewLines) {
  727. return convertToHTML(base.parse(str, preserveNewLines), true);
  728. };
  729. /**
  730. * @private
  731. */
  732. convertToHTML = function(tokens, isRoot) {
  733. var token, bbcode, content, html, needsBlockWrap, blockWrapOpen,
  734. isInline, lastChild,
  735. ret = [];
  736. isInline = function(bbcode) {
  737. return (!bbcode || (typeof bbcode.isHtmlInline !== 'undefined' ? bbcode.isHtmlInline : bbcode.isInline)) !== false;
  738. };
  739. while(tokens.length > 0)
  740. {
  741. if(!(token = tokens.shift()))
  742. continue;
  743. if(token.type === tokenType.open)
  744. {
  745. lastChild = token.children[token.children.length - 1] || {};
  746. bbcode = base.bbcodes[token.name];
  747. needsBlockWrap = isRoot && isInline(bbcode);
  748. content = convertToHTML(token.children, false);
  749. if(bbcode && bbcode.html)
  750. {
  751. // Only add a line break to the end if this is blocklevel and the last child wasn't block-level
  752. if(!isInline(bbcode) && isInline(base.bbcodes[lastChild.name]) && !bbcode.isPreFormatted && !bbcode.skipLastLineBreak)
  753. {
  754. // Add placeholder br to end of block level elements in all browsers apart from IE < 9 which
  755. // handle new lines differently and doesn't need one.
  756. if(!$.sceditor.ie)
  757. content += '<br />';
  758. }
  759. if($.isFunction(bbcode.html))
  760. html = bbcode.html.call(base, token, token.attrs, content);
  761. else
  762. html = $.sceditor.plugins.bbcode.formatString(bbcode.html, content);
  763. }
  764. else
  765. html = token.val + content + (token.closing ? token.closing.val : '');
  766. }
  767. else if(token.type === tokenType.newline)
  768. {
  769. if(!isRoot)
  770. {
  771. ret.push('<br />');
  772. continue;
  773. }
  774. // If not already in a block wrap then start a new block
  775. if(!blockWrapOpen)
  776. {
  777. ret.push('<div>');
  778. // If it's an empty DIV and compatibility mode is below IE8 then
  779. // we must add a non-breaking space to the div otherwise the div
  780. // will be collapsed. Adding a BR works but when you press enter
  781. // to make a newline it suddenly goes back to the normal IE div
  782. // behaviour and creates two lines, one for the newline and one
  783. // for the BR. I'm sure there must be a better fix but I've yet to
  784. // find one.
  785. // Cannot do zoom: 1; or set a height on the div to fix it as that
  786. // causes resize handles to be added to the div when it's clicked on/
  787. if((document.documentMode && document.documentMode < 8) || $.sceditor.ie < 8)
  788. ret.push('\u00a0');
  789. }
  790. // Putting BR in a div in IE causes it to do a double line break.
  791. if(!$.sceditor.ie)
  792. ret.push('<br />');
  793. // Normally the div acts as a line-break with by moving whatever comes
  794. // after onto a new line.
  795. // If this is the last token, add an extra line-break so it shows as
  796. // there will be nothing after it.
  797. if(!tokens.length)
  798. ret.push('<br />');
  799. ret.push('</div>\n');
  800. blockWrapOpen = false;
  801. continue;
  802. }
  803. else // content
  804. {
  805. needsBlockWrap = isRoot;
  806. html = $.sceditor.escapeEntities(token.val);
  807. }
  808. if(needsBlockWrap && !blockWrapOpen)
  809. {
  810. ret.push('<div>');
  811. blockWrapOpen = true;
  812. }
  813. else if(!needsBlockWrap && blockWrapOpen)
  814. {
  815. ret.push('</div>\n');
  816. blockWrapOpen = false;
  817. }
  818. ret.push(html);
  819. }
  820. if(blockWrapOpen)
  821. ret.push('</div>\n');
  822. return ret.join('');
  823. };
  824. /**
  825. * Takes a BBCode string, parses it then converts it back to BBCode.
  826. *
  827. * This will auto fix the BBCode and format it with the specified options.
  828. *
  829. * @param {String} str
  830. * @param {Bool} preserveNewLines If to preserve all new lines, not strip any based on the passed formatting options
  831. * @return {String}
  832. * @memberOf jQuery.sceditor.BBCodeParser.prototype
  833. */
  834. base.toBBCode = function(str, preserveNewLines) {
  835. return convertToBBCode(base.parse(str, preserveNewLines));
  836. };
  837. /**
  838. * Converts parsed tokens back into BBCode with the
  839. * formatting specified in the options and with any
  840. * fixes specified.
  841. *
  842. * @param {Array} toks Array of parsed tokens from base.parse()
  843. * @return {String}
  844. * @private
  845. */
  846. convertToBBCode = function(toks) {
  847. var token, attr, bbcode, isBlock, isSelfClosing, quoteType,
  848. breakBefore, breakStart, breakEnd, breakAfter,
  849. // Create an array of strings which are joined together
  850. // before being returned as this is faster in slow browsers.
  851. // (Old versions of IE).
  852. ret = [];
  853. while(toks.length > 0)
  854. {
  855. if(!(token = toks.shift()))
  856. continue;
  857. bbcode = base.bbcodes[token.name];
  858. isBlock = !(!bbcode || bbcode.isInline !== false);
  859. isSelfClosing = bbcode && bbcode.isSelfClosing;
  860. breakBefore = ((isBlock && base.opts.breakBeforeBlock && bbcode.breakBefore !== false) || (bbcode && bbcode.breakBefore));
  861. breakStart = ((isBlock && !isSelfClosing && base.opts.breakStartBlock && bbcode.breakStart !== false) || (bbcode && bbcode.breakStart));
  862. breakEnd = ((isBlock && base.opts.breakEndBlock && bbcode.breakEnd !== false) || (bbcode && bbcode.breakEnd));
  863. breakAfter = ((isBlock && base.opts.breakAfterBlock && bbcode.breakAfter !== false) || (bbcode && bbcode.breakAfter));
  864. quoteType = (bbcode ? bbcode.quoteType : null) || base.opts.quoteType || $.sceditor.BBCodeParser.QuoteType.auto;
  865. if(!bbcode && token.type === tokenType.open)
  866. {
  867. ret.push(token.val);
  868. if(token.children)
  869. ret.push(convertToBBCode(token.children));
  870. if(token.closing)
  871. ret.push(token.closing.val);
  872. }
  873. else if(token.type === tokenType.open)
  874. {
  875. if(breakBefore)
  876. ret.push('\n');
  877. // Convert the tag and it's attributes to BBCode
  878. ret.push('[' + token.name);
  879. if(token.attrs)
  880. {
  881. if(token.attrs.defaultattr)
  882. {
  883. ret.push('=' + quote(token.attrs.defaultattr, quoteType, 'defaultattr'));
  884. delete token.attrs.defaultattr;
  885. }
  886. for(attr in token.attrs)
  887. if(token.attrs.hasOwnProperty(attr))
  888. ret.push(' ' + attr + '=' + quote(token.attrs[attr], quoteType, attr));
  889. }
  890. ret.push(']');
  891. if(breakStart)
  892. ret.push('\n');
  893. // Convert the tags children to BBCode
  894. if(token.children)
  895. ret.push(convertToBBCode(token.children));
  896. // add closing tag if not self closing
  897. if(!isSelfClosing && !bbcode.excludeClosing)
  898. {
  899. if(breakEnd)
  900. ret.push('\n');
  901. ret.push('[/' + token.name + ']');
  902. }
  903. if(breakAfter)
  904. ret.push('\n');
  905. // preserve whatever was recognised as the closing tag if
  906. // it is a self closing tag
  907. if(token.closing && isSelfClosing)
  908. ret.push(token.closing.val);
  909. }
  910. else
  911. ret.push(token.val);
  912. }
  913. return ret.join('');
  914. };
  915. /**
  916. * Quotes an attribute
  917. *
  918. * @param {String} str
  919. * @param {$.sceditor.BBCodeParser.QuoteType} quoteType
  920. * @param {String} name
  921. * @return {String}
  922. * @private
  923. */
  924. quote = function(str, quoteType, name) {
  925. var QuoteTypes = $.sceditor.BBCodeParser.QuoteType,
  926. needsQuotes = /\s|=/.test(str);
  927. if($.isFunction(quoteType))
  928. return quoteType(str, name);
  929. if(quoteType === QuoteTypes.never || (quoteType === QuoteTypes.auto && !needsQuotes))
  930. return str;
  931. return '"' + str.replace('\\', '\\\\').replace('"', '\\"') + '"';
  932. };
  933. /**
  934. * Returns the last element of an array or null
  935. *
  936. * @param {Array} arr
  937. * @return {Object} Last element
  938. * @private
  939. */
  940. last = function(arr) {
  941. if(arr.length)
  942. return arr[arr.length - 1];
  943. return null;
  944. };
  945. /**
  946. * Converts a string to lowercase.
  947. *
  948. * @param {String} str
  949. * @return {String} Lowercase version of str
  950. * @private
  951. */
  952. lower = function(str) {
  953. return str.toLowerCase();
  954. };
  955. init();
  956. };
  957. /**
  958. * Quote type
  959. * @type {Object}
  960. * @class QuoteType
  961. * @name jQuery.sceditor.BBCodeParser.QuoteType
  962. * @since v1.4.0
  963. */
  964. $.sceditor.BBCodeParser.QuoteType = {
  965. /** @lends jQuery.sceditor.BBCodeParser.QuoteType */
  966. /**
  967. * Always quote the attribute value
  968. * @type {Number}
  969. */
  970. always: 1,
  971. /**
  972. * Never quote the attributes value
  973. * @type {Number}
  974. */
  975. never: 2,
  976. /**
  977. * Only quote the attributes value when it contains spaces to equals
  978. * @type {Number}
  979. */
  980. auto: 3
  981. };
  982. /**
  983. * Default BBCode parser options
  984. * @type {Object}
  985. */
  986. $.sceditor.BBCodeParser.defaults = {
  987. /**
  988. * If to add a new line before block level elements
  989. * @type {Boolean}
  990. */
  991. breakBeforeBlock: false,
  992. /**
  993. * If to add a new line after the start of block level elements
  994. * @type {Boolean}
  995. */
  996. breakStartBlock: false,
  997. /**
  998. * If to add a new line before the end of block level elements
  999. * @type {Boolean}
  1000. */
  1001. breakEndBlock: false,
  1002. /**
  1003. * If to add a new line after block level elements
  1004. * @type {Boolean}
  1005. */
  1006. breakAfterBlock: true,
  1007. /**
  1008. * If to remove empty tags
  1009. * @type {Boolean}
  1010. */
  1011. removeEmptyTags: true,
  1012. /**
  1013. * If to fix invalid nesting, i.e. block level elements inside inline elements.
  1014. * @type {Boolean}
  1015. */
  1016. fixInvalidNesting: true,
  1017. /**
  1018. * If to fix invalid children. i.e. A tag which is inside a parent that doesn't allow that type of tag.
  1019. * @type {Boolean}
  1020. */
  1021. fixInvalidChildren: true,
  1022. /**
  1023. * Attribute quote type
  1024. * @type {$.sceditor.BBCodeParser.QuoteType}
  1025. * @since 1.4.1
  1026. */
  1027. quoteType: $.sceditor.BBCodeParser.QuoteType.auto
  1028. };
  1029. /**
  1030. * Deprecated, use $.sceditor.plugins.bbcode
  1031. *
  1032. * @class sceditorBBCodePlugin
  1033. * @name jQuery.sceditor.sceditorBBCodePlugin
  1034. * @deprecated
  1035. */
  1036. $.sceditorBBCodePlugin =
  1037. /**
  1038. * BBCode plugin for SCEditor
  1039. *
  1040. * @class bbcode
  1041. * @name jQuery.sceditor.plugins.bbcode
  1042. * @since 1.4.1
  1043. */
  1044. $.sceditor.plugins.bbcode = function() {
  1045. var base = this;
  1046. /**
  1047. * Private methods
  1048. * @private
  1049. */
  1050. var buildBbcodeCache,
  1051. handleStyles,
  1052. handleTags,
  1053. formatString,
  1054. getStyle,
  1055. mergeSourceModeCommands,
  1056. removeFirstLastDiv;
  1057. formatString = $.sceditor.plugins.bbcode.formatString;
  1058. base.bbcodes = $.sceditor.plugins.bbcode.bbcodes;
  1059. base.stripQuotes = $.sceditor.plugins.bbcode.stripQuotes;
  1060. /**
  1061. * cache of all the tags pointing to their bbcodes to enable
  1062. * faster lookup of which bbcode a tag should have
  1063. * @private
  1064. */
  1065. var tagsToBbcodes = {};
  1066. /**
  1067. * Same as tagsToBbcodes but instead of HTML tags it's styles
  1068. * @private
  1069. */
  1070. var stylesToBbcodes = {};
  1071. /**
  1072. * Allowed children of specific HTML tags. Empty array if no
  1073. * children other than text nodes are allowed
  1074. * @private
  1075. */
  1076. var validChildren = {
  1077. ul: ['li', 'ol', 'ul'],
  1078. ol: ['li', 'ol', 'ul'],
  1079. table: ['tr'],
  1080. tr: ['td', 'th'],
  1081. code: ['br', 'p', 'div']
  1082. };
  1083. /**
  1084. * Cache of CamelCase versions of CSS properties
  1085. * @type {Object}
  1086. */
  1087. var propertyCache = {};
  1088. /**
  1089. * Initializer
  1090. * @private
  1091. */
  1092. base.init = function() {
  1093. base.opts = this.opts;
  1094. // build the BBCode cache
  1095. buildBbcodeCache();
  1096. mergeSourceModeCommands(this);
  1097. // Add BBCode helper methods
  1098. this.toBBCode = base.signalToSource;
  1099. this.fromBBCode = base.signalToWysiwyg;
  1100. };
  1101. mergeSourceModeCommands = function(editor) {
  1102. var getCommand = $.sceditor.command.get;
  1103. var merge = {
  1104. bold: { txtExec: ['[b]', '[/b]'] },
  1105. italic: { txtExec: ['[i]', '[/i]'] },
  1106. underline: { txtExec: ['[u]', '[/u]'] },
  1107. strike: { txtExec: ['[s]', '[/s]'] },
  1108. subscript: { txtExec: ['[sub]', '[/sub]'] },
  1109. superscript: { txtExec: ['[sup]', '[/sup]'] },
  1110. left: { txtExec: ['[left]', '[/left]'] },
  1111. center: { txtExec: ['[center]', '[/center]'] },
  1112. right: { txtExec: ['[right]', '[/right]'] },
  1113. justify: { txtExec: ['[justify]', '[/justify]'] },
  1114. font: {
  1115. txtExec: function(caller) {
  1116. var editor = this;
  1117. getCommand('font')._dropDown(
  1118. editor,
  1119. caller,
  1120. function(fontName) {
  1121. editor.insertText('[font='+fontName+']', '[/font]');
  1122. }
  1123. );
  1124. }
  1125. },
  1126. size: {
  1127. txtExec: function(caller) {
  1128. var editor = this;
  1129. getCommand('size')._dropDown(
  1130. editor,
  1131. caller,
  1132. function(fontSize) {
  1133. editor.insertText('[size='+fontSize+']', '[/size]');
  1134. }
  1135. );
  1136. }
  1137. },
  1138. color: {
  1139. txtExec: function(caller) {
  1140. var editor = this;
  1141. getCommand('color')._dropDown(
  1142. editor,
  1143. caller,
  1144. function(color) {
  1145. editor.insertText('[color='+color+']', '[/color]');
  1146. }
  1147. );
  1148. }
  1149. },
  1150. bulletlist: {
  1151. txtExec: function(caller, selected) {
  1152. var content = '';
  1153. $.each(selected.split(/\r?\n/), function() {
  1154. content += (content ? '\n' : '') + '[li]' + this + '[/li]';
  1155. });
  1156. editor.insertText('[ul]\n' + content + '\n[/ul]');
  1157. }
  1158. },
  1159. orderedlist: {
  1160. txtExec: function(caller, selected) {
  1161. var content = '';
  1162. $.each(selected.split(/\r?\n/), function() {
  1163. content += (content ? '\n' : '') + '[li]' + this + '[/li]';
  1164. });
  1165. $.sceditor.plugins.bbcode.bbcode.get('');
  1166. editor.insertText('[ol]\n' + content + '\n[/ol]');
  1167. }
  1168. },
  1169. table: { txtExec: ['[table][tr][td]', '[/td][/tr][/table]'] },
  1170. horizontalrule: { txtExec: ['[hr]'] },
  1171. code: { txtExec: ['[code]', '[/code]'] },
  1172. image: {
  1173. txtExec: function(caller, selected) {
  1174. var url = prompt(this._('Enter the image URL:'), selected);
  1175. if(url)
  1176. this.insertText('[img]' + url + '[/img]');
  1177. }
  1178. },
  1179. email: {
  1180. txtExec: function(caller, selected) {
  1181. var display = selected && selected.indexOf('@') > -1 ? null : selected,
  1182. email = prompt(this._('Enter the e-mail address:'), (display ? '' : selected)),
  1183. text = prompt(this._('Enter the displayed text:'), display || email) || email;
  1184. if(email)
  1185. this.insertText('[email=' + email + ']' + text + '[/email]');
  1186. }
  1187. },
  1188. link: {
  1189. txtExec: function(caller, selected) {
  1190. var display = selected && selected.indexOf('http://') > -1 ? null : selected,
  1191. url = prompt(this._('Enter URL:'), (display ? 'http://' : selected)),
  1192. text = prompt(this._('Enter the displayed text:'), display || url) || url;
  1193. if(url)
  1194. this.insertText('[url=' + url + ']' + text + '[/url]');
  1195. }
  1196. },
  1197. quote: { txtExec: ['[quote]', '[/quote]'] },
  1198. youtube: {
  1199. txtExec: function(caller) {
  1200. var editor = this;
  1201. getCommand('youtube')._dropDown(
  1202. editor,
  1203. caller,
  1204. function(id) {
  1205. editor.insertText('[youtube]' + id + '[/youtube]');
  1206. }
  1207. );
  1208. }
  1209. },
  1210. rtl: { txtExec: ['[rtl]', '[/rtl]'] },
  1211. ltr: { txtExec: ['[ltr]', '[/ltr]'] }
  1212. };
  1213. editor.commands = $.extend(true, {}, merge, editor.commands);
  1214. };
  1215. /**
  1216. * Populates tagsToBbcodes and stylesToBbcodes to enable faster lookups
  1217. *
  1218. * @private
  1219. */
  1220. buildBbcodeCache = function() {
  1221. $.each(base.bbcodes, function(bbcode) {
  1222. if(base.bbcodes[bbcode].tags)
  1223. $.each(base.bbcodes[bbcode].tags, function(tag, values) {
  1224. var isBlock = base.bbcodes[bbcode].isInline === false;
  1225. tagsToBbcodes[tag] = (tagsToBbcodes[tag] || {});
  1226. tagsToBbcodes[tag][isBlock] = (tagsToBbcodes[tag][isBlock] || {});
  1227. tagsToBbcodes[tag][isBlock][bbcode] = values;
  1228. });
  1229. if(base.bbcodes[bbcode].styles)
  1230. $.each(base.bbcodes[bbcode].styles, function(style, values) {
  1231. var isBlock = base.bbcodes[bbcode].isInline === false;
  1232. stylesToBbcodes[isBlock] = (stylesToBbcodes[isBlock] || {});
  1233. stylesToBbcodes[isBlock][style] = (stylesToBbcodes[isBlock][style] || {});
  1234. stylesToBbcodes[isBlock][style][bbcode] = values;
  1235. });
  1236. });
  1237. };
  1238. /**
  1239. * Gets the value of a style property on the passed element
  1240. * @private
  1241. */
  1242. getStyle = function(element, property) {
  1243. var $elm, ret, dir, textAlign, name,
  1244. style = element.style;
  1245. if(!style)
  1246. return null;
  1247. if(!propertyCache[property])
  1248. propertyCache[property] = $.camelCase(property);
  1249. name = propertyCache[property];
  1250. // add exception for align
  1251. if('text-align' === property)
  1252. {
  1253. $elm = $(element);
  1254. dir = style.direction;
  1255. textAlign = style[name] || $elm.css(property);
  1256. // @SMF code: TBH I don't remember why this was added, though it should be useful somehow
  1257. if(!element.style)
  1258. return null;
  1259. if($elm.parent().css(property) !== textAlign &&
  1260. $elm.css('display') === 'block' && !$elm.is('hr') && !$elm.is('th'))
  1261. ret = textAlign;
  1262. // IE changes text-align to the same as the current direction so skip unless overridden by user
  1263. if(dir && ret && ((/right/i.test(ret) && dir === 'rtl') || (/left/i.test(ret) && dir === 'ltr')))
  1264. return null;
  1265. return ret;
  1266. }
  1267. return style[name];
  1268. };
  1269. /**
  1270. * Checks if any bbcode styles match the elements styles
  1271. *
  1272. * @return string Content with any matching bbcode tags wrapped around it.
  1273. * @private
  1274. */
  1275. handleStyles = function($element, content, blockLevel) {
  1276. var elementPropVal;
  1277. // convert blockLevel to boolean
  1278. blockLevel = !!blockLevel;
  1279. if(!stylesToBbcodes[blockLevel])
  1280. return content;
  1281. $.each(stylesToBbcodes[blockLevel], function(property, bbcodes) {
  1282. elementPropVal = getStyle($element[0], property);
  1283. // if the parent has the same style use that instead of this one
  1284. // so you don't end up with [i]parent[i]child[/i][/i]
  1285. if(!elementPropVal || getStyle($element.parent()[0], property) === elementPropVal)
  1286. return;
  1287. $.each(bbcodes, function(bbcode, values) {
  1288. if(!values || $.inArray(elementPropVal.toString(), values) > -1)
  1289. {
  1290. if($.isFunction(base.bbcodes[bbcode].format))
  1291. content = base.bbcodes[bbcode].format.call(base, $element, content);
  1292. else
  1293. content = formatString(base.bbcodes[bbcode].format, content);
  1294. }
  1295. });
  1296. });
  1297. return content;
  1298. };
  1299. /**
  1300. * Handles a HTML tag and finds any matching bbcodes
  1301. *
  1302. * @param {jQuery} element The element to convert
  1303. * @param {String} content The Tags text content
  1304. * @param {Bool} blockLevel If to convert block level tags
  1305. * @return {String} Content with any matching bbcode tags wrapped around it.
  1306. * @private
  1307. */
  1308. handleTags = function($element, content, blockLevel) {
  1309. var convertBBCode,
  1310. element = $element[0],
  1311. tag = element.nodeName.toLowerCase();
  1312. // convert blockLevel to boolean
  1313. blockLevel = !!blockLevel;
  1314. if(tagsToBbcodes[tag] && tagsToBbcodes[tag][blockLevel]) {
  1315. // loop all bbcodes for this tag
  1316. $.each(tagsToBbcodes[tag][blockLevel], function(bbcode, bbcodeAttribs) {
  1317. // if the bbcode requires any attributes then check this has
  1318. // all needed
  1319. if(bbcodeAttribs)
  1320. {
  1321. convertBBCode = false;
  1322. // loop all the bbcode attribs
  1323. $.each(bbcodeAttribs, function(attrib, values) {
  1324. // if the $element has the bbcodes attribute and the bbcode attribute
  1325. // has values check one of the values matches
  1326. if(!$element.attr(attrib) || (values && $.inArray($element.attr(attrib), values) < 0))
  1327. return;
  1328. // break this loop as we have matched this bbcode
  1329. convertBBCode = true;
  1330. return false;
  1331. });
  1332. if(!convertBBCode)
  1333. return;
  1334. }
  1335. if($.isFunction(base.bbcodes[bbcode].format))
  1336. content = base.bbcodes[bbcode].format.call(base, $element, content);
  1337. else
  1338. content = formatString(base.bbcodes[bbcode].format, content);
  1339. });
  1340. }
  1341. if(blockLevel && (!$.sceditor.dom.isInline(element, true) || tag === 'br'))
  1342. {
  1343. var parent = element.parentNode,
  1344. parentLastChild = parent.lastChild,
  1345. previousSibling = element.previousSibling,
  1346. parentIsInline = $.sceditor.dom.isInline(parent, true);
  1347. // skips selection makers and other ignored items
  1348. while(previousSibling && $(previousSibling).hasClass('sceditor-ignore'))
  1349. previousSibling = previousSibling.previousSibling;
  1350. while($(parentLastChild).hasClass('sceditor-ignore'))
  1351. parentLastChild = parentLastChild.previousSibling;
  1352. // If this is
  1353. // A br/block element inside an inline element.
  1354. // The last block level as the last block level is collapsed.
  1355. // Is an li element.
  1356. // Is IE and the tag is BR. IE never collapses BR's
  1357. if(parentIsInline || parentLastChild !== element || tag === 'li' || (tag === 'br' && $.sceditor.ie))
  1358. content += '\n';
  1359. // Check for <div>text<div>This needs a newline prepended</div></div>
  1360. if('br' !== tag && previousSibling && previousSibling.nodeName.toLowerCase() !== 'br' && $.sceditor.dom.isInline(previousSibling, true))
  1361. content = '\n' + content;
  1362. }
  1363. return content;
  1364. };
  1365. /**
  1366. * Converts HTML to BBCode
  1367. * @param string html Html string, this function ignores this, it works off domBody
  1368. * @param HtmlElement $body Editors dom body object to convert
  1369. * @return string BBCode which has been converted from HTML
  1370. * @memberOf jQuery.plugins.bbcode.prototype
  1371. */
  1372. base.signalToSource = function(html, $body) {
  1373. var $tmpContainer, bbcode,
  1374. parser = new $.sceditor.BBCodeParser(base.opts.parserOptions);
  1375. if(!$body)
  1376. {
  1377. if(typeof html === 'string')
  1378. {
  1379. $tmpContainer = $('<div />').css('visibility', 'hidden').appendTo(document.body).html(html);
  1380. $body = $tmpContainer;
  1381. }
  1382. else
  1383. $body = $(html);
  1384. }
  1385. if(!$body || !$body.jquery)
  1386. return '';
  1387. $.sceditor.dom.removeWhiteSpace($body[0]);
  1388. bbcode = base.elementToBbcode($body);
  1389. if($tmpContainer)
  1390. $tmpContainer.remove();
  1391. bbcode = parser.toBBCode(bbcode, true);
  1392. if(base.opts.bbcodeTrim)
  1393. bbcode = $.trim(bbcode);
  1394. return bbcode;
  1395. };
  1396. /**
  1397. * Converts a HTML dom element to BBCode starting from
  1398. * the innermost element and working backwards
  1399. *
  1400. * @private
  1401. * @param HtmlElement element The element to convert to BBCode
  1402. * @param array vChildren Valid child tags allowed
  1403. * @return string BBCode
  1404. * @memberOf jQuery.plugins.bbcode.prototype
  1405. */
  1406. base.elementToBbcode = function($element) {
  1407. return (function toBBCode(node, vChildren) {
  1408. var ret = '';
  1409. // TODO: Move to BBCode class?
  1410. $.sceditor.dom.traverse(node, function(node) {
  1411. var $node = $(node),
  1412. curTag = '',
  1413. nodeType = node.nodeType,
  1414. tag = node.nodeName.toLowerCase(),
  1415. vChild = validChildren[tag],
  1416. firstChild = node.firstChild,
  1417. isValidChild = true;
  1418. if(typeof vChildren === 'object')
  1419. {
  1420. isValidChild = $.inArray(tag, vChildren) > -1;
  1421. // Emoticons should always be converted
  1422. if($node.is('img') && $node.data('sceditor-emoticon'))
  1423. isValidChild = true;
  1424. // if this tag is one of the parents allowed children
  1425. // then set this tags allowed children to whatever it allows,
  1426. // otherwise set to what the parent allows
  1427. if(!isValidChild)
  1428. vChild = vChildren;
  1429. }
  1430. // 3 = text and 1 = element
  1431. if(nodeType !== 3 && nodeType !== 1)
  1432. return;
  1433. if(nodeType === 1)
  1434. {
  1435. // skip ignored elements
  1436. if($node.hasClass('sceditor-ignore'))
  1437. return;
  1438. // skip empty nlf elements (new lines automatically added after block level elements like quotes)
  1439. if($node.hasClass('sceditor-nlf'))
  1440. {
  1441. if(!firstChild || (!$.sceditor.ie && node.childNodes.length === 1 && /br/i.test(firstChild.nodeName)))
  1442. {
  1443. return;
  1444. }
  1445. }
  1446. // don't loop inside iframes
  1447. if(tag !== 'iframe')
  1448. curTag = toBBCode(node, vChild);
  1449. // TODO: isValidChild is no longer needed. Should use valid children bbcodes instead by
  1450. // creating BBCode tokens like the parser.
  1451. if(isValidChild)
  1452. {
  1453. // code tags should skip most styles
  1454. if(tag !== 'code')
  1455. {
  1456. // handle inline bbcodes
  1457. curTag = handleStyles($node, curTag);
  1458. curTag = handleTags($node, curTag);
  1459. // handle blocklevel bbcodes
  1460. curTag = handleStyles($node, curTag, true);
  1461. }
  1462. ret += handleTags($node, curTag, true);
  1463. }
  1464. else
  1465. ret += curTag;
  1466. }
  1467. else if(node.wholeText && (!node.previousSibling || node.previousSibling.nodeType !== 3))
  1468. {
  1469. // TODO:This should check for CSS white-space, should pass it in the function to reduce css lookups which are SLOW!
  1470. if($node.parents('code').length === 0)
  1471. ret += node.wholeText.replace(/ +/g, " ");
  1472. else
  1473. ret += node.wholeText;
  1474. }
  1475. else if(!node.wholeText)
  1476. ret += node.nodeValue;
  1477. }, false, true);
  1478. return ret;
  1479. }($element[0]));
  1480. };
  1481. /**
  1482. * Converts BBCode to HTML
  1483. *
  1484. * @param {String} text
  1485. * @param {Bool} asFragment
  1486. * @return {String} HTML
  1487. * @memberOf jQuery.plugins.bbcode.prototype
  1488. */
  1489. base.signalToWysiwyg = function(text, asFragment) {
  1490. var parser = new $.sceditor.BBCodeParser(base.opts.parserOptions),
  1491. html = parser.toHTML(base.opts.bbcodeTrim ? $.trim(text) : text);
  1492. return asFragment ? removeFirstLastDiv(html) : html;
  1493. };
  1494. /**
  1495. * Removes the first and last divs from the HTML.
  1496. *
  1497. * This is needed for pasting
  1498. * @param {String} html
  1499. * @return {String}
  1500. * @private
  1501. */
  1502. removeFirstLastDiv = function(html) {
  1503. var node, next, removeDiv,
  1504. $output = $('<div />').hide().appendTo(document.body),
  1505. output = $output[0];
  1506. removeDiv = function(node, isFirst) {
  1507. // Don't remove divs that have styling
  1508. if($.sceditor.dom.hasStyling(node))
  1509. return;
  1510. if($.sceditor.ie || (node.childNodes.length !== 1 || !$(node.firstChild).is('br')))
  1511. {
  1512. while((next = node.firstChild))
  1513. output.insertBefore(next, node);
  1514. }
  1515. if(isFirst)
  1516. {
  1517. var lastChild = output.lastChild;
  1518. if(node !== lastChild && $(lastChild).is('div') && node.nextSibling === lastChild)
  1519. output.insertBefore(document.createElement('br'), node);
  1520. }
  1521. output.removeChild(node);
  1522. };
  1523. output.innerHTML = html.replace(/<\/div>\n/g, '</div>');
  1524. if((node = output.firstChild) && $(node).is('div'))
  1525. removeDiv(node, true);
  1526. if((node = output.lastChild) && $(node).is('div'))
  1527. removeDiv(node);
  1528. output = output.innerHTML;
  1529. $output.remove();
  1530. return output;
  1531. };
  1532. };
  1533. /**
  1534. * Removes any leading or trailing quotes ('")
  1535. *
  1536. * @return string
  1537. * @since v1.4.0
  1538. */
  1539. $.sceditor.plugins.bbcode.stripQuotes = function(str) {
  1540. return str ? str.replace(/\\(.)/g, '$1').replace(/^(["'])(.*?)\1$/, '$2') : str;
  1541. };
  1542. /**
  1543. * Formats a string replacing {0}, {1}, {2}, ect. with
  1544. * the params provided
  1545. *
  1546. * @param {String} str The string to format
  1547. * @param {string} args... The strings to replace
  1548. * @return {String}
  1549. * @since v1.4.0
  1550. */
  1551. $.sceditor.plugins.bbcode.formatString = function() {
  1552. var args = arguments;
  1553. return args[0].replace(/\{(\d+)\}/g, function(str, p1) {
  1554. return typeof args[p1-0+1] !== 'undefined' ?
  1555. args[p1-0+1] :
  1556. '{' + p1 + '}';
  1557. });
  1558. };
  1559. /**
  1560. * Converts CSS RGB and hex shorthand into hex
  1561. *
  1562. * @since v1.4.0
  1563. * @param {String} color
  1564. * @return {String}
  1565. */
  1566. var normaliseColour = $.sceditor.plugins.bbcode.normaliseColour = function(color) {
  1567. var m, toHex;
  1568. toHex = function (n) {
  1569. n = parseInt(n, 10);
  1570. if(isNaN(n))
  1571. return '00';
  1572. n = Math.max(0, Math.min(n, 255)).toString(16);
  1573. return n.length < 2 ? '0' + n : n;
  1574. };
  1575. color = color || '#000';
  1576. // rgb(n,n,n);
  1577. if((m = color.match(/rgb\((\d{1,3}),\s*?(\d{1,3}),\s*?(\d{1,3})\)/i)))
  1578. return '#' + toHex(m[1]) + toHex(m[2]-0) + toHex(m[3]-0);
  1579. // expand shorthand
  1580. if((m = color.match(/#([0-f])([0-f])([0-f])\s*?$/i)))
  1581. return '#' + m[1] + m[1] + m[2] + m[2] + m[3] + m[3];
  1582. return color;
  1583. };
  1584. $.sceditor.plugins.bbcode.bbcodes = {
  1585. // START_COMMAND: Bold
  1586. b: {
  1587. tags: {
  1588. b: null,
  1589. strong: null
  1590. },
  1591. styles: {
  1592. // 401 is for FF 3.5
  1593. 'font-weight': ['bold', 'bolder', '401', '700', '800', '900']
  1594. },
  1595. format: '[b]{0}[/b]',
  1596. html: '<strong>{0}</strong>'
  1597. },
  1598. // END_COMMAND
  1599. // START_COMMAND: Italic
  1600. i: {
  1601. tags: {
  1602. i: null,
  1603. em: null
  1604. },
  1605. styles: {
  1606. 'font-style': ['italic', 'oblique']
  1607. },
  1608. format: "[i]{0}[/i]",
  1609. html: '<em>{0}</em>'
  1610. },
  1611. // END_COMMAND
  1612. // START_COMMAND: Underline
  1613. u: {
  1614. tags: {
  1615. u: null
  1616. },
  1617. styles: {
  1618. 'text-decoration': ['underline']
  1619. },
  1620. format: '[u]{0}[/u]',
  1621. html: '<u>{0}</u>'
  1622. },
  1623. // END_COMMAND
  1624. // START_COMMAND: Strikethrough
  1625. s: {
  1626. tags: {
  1627. s: null,
  1628. strike: null
  1629. },
  1630. styles: {
  1631. 'text-decoration': ['line-through']
  1632. },
  1633. format: '[s]{0}[/s]',
  1634. html: '<s>{0}</s>'
  1635. },
  1636. // END_COMMAND
  1637. // START_COMMAND: Subscript
  1638. sub: {
  1639. tags: {
  1640. sub: null
  1641. },
  1642. format: '[sub]{0}[/sub]',
  1643. html: '<sub>{0}</sub>'
  1644. },
  1645. // END_COMMAND
  1646. // START_COMMAND: Superscript
  1647. sup: {
  1648. tags: {
  1649. sup: null
  1650. },
  1651. format: '[sup]{0}[/sup]',
  1652. html: '<sup>{0}</sup>'
  1653. },
  1654. // END_COMMAND
  1655. // START_COMMAND: Font
  1656. font: {
  1657. tags: {
  1658. font: {
  1659. face: null
  1660. }
  1661. },
  1662. styles: {
  1663. 'font-family': null
  1664. },
  1665. quoteType: $.sceditor.BBCodeParser.QuoteType.never,
  1666. format: function(element, content) {
  1667. var font;
  1668. if(element[0].nodeName.toLowerCase() !== 'font' || !(font = element.attr('face')))
  1669. font = element.css('font-family');
  1670. return '[font=' + this.stripQuotes(font) + ']' + content + '[/font]';
  1671. },
  1672. html: function(token, attrs, content) {
  1673. return '<font face="' + attrs.defaultattr + '">' + content + '</font>';
  1674. }
  1675. },
  1676. // END_COMMAND
  1677. // START_COMMAND: Size
  1678. size: {
  1679. tags: {
  1680. font: {
  1681. size: null
  1682. }
  1683. },
  1684. styles: {
  1685. 'font-size': null
  1686. },
  1687. format: function(element, content) {
  1688. var fontSize = element.attr('size'),
  1689. size = 1;
  1690. if(!fontSize)
  1691. fontSize = element.css('fontSize');
  1692. // Most browsers return px value but IE returns 1-7
  1693. if(fontSize.indexOf('px') > -1) {
  1694. // convert size to an int
  1695. fontSize = fontSize.replace('px', '') - 0;
  1696. if(fontSize > 12)
  1697. size = 2;
  1698. if(fontSize > 15)
  1699. size = 3;
  1700. if(fontSize > 17)
  1701. size = 4;
  1702. if(fontSize > 23)
  1703. size = 5;
  1704. if(fontSize > 31)
  1705. size = 6;
  1706. if(fontSize > 47)
  1707. size = 7;
  1708. }
  1709. else
  1710. size = fontSize;
  1711. return '[size=' + size + ']' + content + '[/size]';
  1712. },
  1713. html: function(token, attrs, content) {
  1714. return '<font size="' + attrs.defaultattr + '">' + content + '</font>';
  1715. }
  1716. },
  1717. // END_COMMAND
  1718. // START_COMMAND: Color
  1719. color: {
  1720. tags: {
  1721. font: {
  1722. color: null
  1723. }
  1724. },
  1725. styles: {
  1726. color: null
  1727. },
  1728. quoteType: $.sceditor.BBCodeParser.QuoteType.never,
  1729. format: function($element, content) {
  1730. var color,
  1731. element = $element[0];
  1732. if(element.nodeName.toLowerCase() !== 'font' || !(color = $element.attr('color')))
  1733. color = element.style.color || $element.css('color');
  1734. return '[color=' + normaliseColour(color) + ']' + content + '[/color]';
  1735. },
  1736. html: function(token, attrs, content) {
  1737. return '<font color="' + normaliseColour(attrs.defaultattr) + '">' + content + '</font>';
  1738. }
  1739. },
  1740. // END_COMMAND
  1741. // START_COMMAND: Lists
  1742. ul: {
  1743. tags: {
  1744. ul: null
  1745. },
  1746. breakStart: true,
  1747. isInline: false,
  1748. skipLastLineBreak: true,
  1749. format: '[ul]{0}[/ul]',
  1750. html: '<ul>{0}</ul>'
  1751. },
  1752. list: {
  1753. breakStart: true,
  1754. isInline: false,
  1755. skipLastLineBreak: true,
  1756. html: '<ul>{0}</ul>'
  1757. },
  1758. ol: {
  1759. tags: {
  1760. ol: null
  1761. },
  1762. breakStart: true,
  1763. isInline: false,
  1764. skipLastLineBreak: true,
  1765. format: '[ol]{0}[/ol]',
  1766. html: '<ol>{0}</ol>'
  1767. },
  1768. li: {
  1769. tags: {
  1770. li: null
  1771. },
  1772. isInline: false,
  1773. closedBy: ['/ul', '/ol', '/list', '*', 'li'],
  1774. format: '[li]{0}[/li]',
  1775. html: '<li>{0}</li>'
  1776. },
  1777. '*': {
  1778. isInline: false,
  1779. closedBy: ['/ul', '/ol', '/list', '*', 'li'],
  1780. html: '<li>{0}</li>'
  1781. },
  1782. // END_COMMAND
  1783. // START_COMMAND: Table
  1784. table: {
  1785. tags: {
  1786. table: null
  1787. },
  1788. isInline: false,
  1789. isHtmlInline: true,
  1790. skipLastLineBreak: true,
  1791. format: '[table]{0}[/table]',
  1792. html: '<table>{0}</table>'
  1793. },
  1794. tr: {
  1795. tags: {
  1796. tr: null
  1797. },
  1798. isInline: false,
  1799. skipLastLineBreak: true,
  1800. format: '[tr]{0}[/tr]',
  1801. html: '<tr>{0}</tr>'
  1802. },
  1803. th: {
  1804. tags: {
  1805. th: null
  1806. },
  1807. allowsEmpty: true,
  1808. isInline: false,
  1809. format: '[th]{0}[/th]',
  1810. html: '<th>{0}</th>'
  1811. },
  1812. td: {
  1813. tags: {
  1814. td: null
  1815. },
  1816. allowsEmpty: true,
  1817. isInline: false,
  1818. format: '[td]{0}[/td]',
  1819. html: '<td>{0}</td>'
  1820. },
  1821. // END_COMMAND
  1822. // START_COMMAND: Emoticons
  1823. emoticon: {
  1824. allowsEmpty: true,
  1825. tags: {
  1826. img: {
  1827. src: null,
  1828. 'data-sceditor-emoticon': null
  1829. }
  1830. },
  1831. format: function(element, content) {
  1832. return element.data('sceditor-emoticon') + content;
  1833. },
  1834. html: '{0}'
  1835. },
  1836. // END_COMMAND
  1837. // START_COMMAND: Horizontal Rule
  1838. hr: {
  1839. tags: {
  1840. hr: null
  1841. },
  1842. allowsEmpty: true,
  1843. isSelfClosing: true,
  1844. isInline: false,
  1845. format: '[hr]{0}',
  1846. html: '<hr />'
  1847. },
  1848. // END_COMMAND
  1849. // START_COMMAND: Image
  1850. img: {
  1851. allowsEmpty: true,
  1852. tags: {
  1853. img: {
  1854. src: null
  1855. }
  1856. },
  1857. quoteType: $.sceditor.BBCodeParser.QuoteType.never,
  1858. format: function($element, content) {
  1859. var w, h,
  1860. attribs = '',
  1861. element = $element[0],
  1862. style = function(name) {
  1863. return element.style ? element.style[name] : null;
  1864. };
  1865. // check if this is an emoticon image
  1866. if($element.attr('data-sceditor-emoticon'))
  1867. return content;
  1868. w = $element.attr('width') || style('width');
  1869. h = $element.attr('height') || style('height');
  1870. // only add width and height if one is specified
  1871. if((element.complete && (w || h)) || (w && h))
  1872. attribs = "=" + $element.width() + "x" + $element.height();
  1873. return '[img' + attribs + ']' + $element.attr('src') + '[/img]';
  1874. },
  1875. html: function(token, attrs, content) {
  1876. var parts,
  1877. attribs = '';
  1878. // handle [img width=340 height=240]url[/img]
  1879. if(typeof attrs.width !== "undefined")
  1880. attribs += ' width="' + attrs.width + '"';
  1881. if(typeof attrs.height !== "undefined")
  1882. attribs += ' height="' + attrs.height + '"';
  1883. // handle [img=340x240]url[/img]
  1884. if(attrs.defaultattr) {
  1885. parts = attrs.defaultattr.split(/x/i);
  1886. attribs = ' width="' + parts[0] + '"' +
  1887. ' height="' + (parts.length === 2 ? parts[1] : parts[0]) + '"';
  1888. }
  1889. return '<img' + attribs + ' src="' + content + '" />';
  1890. }
  1891. },
  1892. // END_COMMAND
  1893. // START_COMMAND: URL
  1894. url: {
  1895. allowsEmpty: true,
  1896. tags: {
  1897. a: {
  1898. href: null
  1899. }
  1900. },
  1901. quoteType: $.sceditor.BBCodeParser.QuoteType.never,
  1902. format: function(element, content) {
  1903. var url = element.attr('href');
  1904. // make sure this link is not an e-mail, if it is return e-mail BBCode
  1905. if(url.substr(0, 7) === 'mailto:')
  1906. return '[email="' + url.substr(7) + '"]' + content + '[/email]';
  1907. return '[url=' + decodeURI(url) + ']' + content + '[/url]';
  1908. },
  1909. html: function(token, attrs, content) {
  1910. return '<a href="' + encodeURI(attrs.defaultattr || content) + '">' + content + '</a>';
  1911. }
  1912. },
  1913. // END_COMMAND
  1914. // START_COMMAND: E-mail
  1915. email: {
  1916. quoteType: $.sceditor.BBCodeParser.QuoteType.never,
  1917. html: function(token, attrs, content) {
  1918. return '<a href="mailto:' + (attrs.defaultattr || content) + '">' + content + '</a>';
  1919. }
  1920. },
  1921. // END_COMMAND
  1922. // START_COMMAND: Quote
  1923. quote: {
  1924. tags: {
  1925. blockquote: null
  1926. },
  1927. isInline: false,
  1928. quoteType: $.sceditor.BBCodeParser.QuoteType.never,
  1929. format: function(element, content) {
  1930. var author = '',
  1931. $elm = $(element),
  1932. $cite = $elm.children('cite').first();
  1933. if($cite.length === 1 || $elm.data('author'))
  1934. {
  1935. author = $cite.text() || $elm.data('author');
  1936. $elm.data('author', author);
  1937. $cite.remove();
  1938. content = this.elementToBbcode($(element));
  1939. author = '=' + author;
  1940. $elm.prepend($cite);
  1941. }
  1942. return '[quote' + author + ']' + content + '[/quote]';
  1943. },
  1944. html: function(token, attrs, content) {
  1945. if(attrs.defaultattr)
  1946. content = '<cite>' + attrs.defaultattr + '</cite>' + content;
  1947. return '<blockquote>' + content + '</blockquote>';
  1948. }
  1949. },
  1950. // END_COMMAND
  1951. // START_COMMAND: Code
  1952. code: {
  1953. tags: {
  1954. code: null
  1955. },
  1956. isInline: false,
  1957. allowedChildren: ['#', '#newline'],
  1958. format: '[code]{0}[/code]',
  1959. html: '<code>{0}</code>'
  1960. },
  1961. // END_COMMAND
  1962. // START_COMMAND: Left
  1963. left: {
  1964. styles: {
  1965. 'text-align': ['left', '-webkit-left', '-moz-left', '-khtml-left']
  1966. },
  1967. isInline: false,
  1968. format: '[left]{0}[/left]',
  1969. html: '<div align="left">{0}</div>'
  1970. },
  1971. // END_COMMAND
  1972. // START_COMMAND: Centre
  1973. center: {
  1974. styles: {
  1975. 'text-align': ['center', '-webkit-center', '-moz-center', '-khtml-center']
  1976. },
  1977. isInline: false,
  1978. format: '[center]{0}[/center]',
  1979. html: '<div align="center">{0}</div>'
  1980. },
  1981. // END_COMMAND
  1982. // START_COMMAND: Right
  1983. right: {
  1984. styles: {
  1985. 'text-align': ['right', '-webkit-right', '-moz-right', '-khtml-right']
  1986. },
  1987. isInline: false,
  1988. format: '[right]{0}[/right]',
  1989. html: '<div align="right">{0}</div>'
  1990. },
  1991. // END_COMMAND
  1992. // START_COMMAND: Justify
  1993. justify: {
  1994. styles: {
  1995. 'text-align': ['justify', '-webkit-justify', '-moz-justify', '-khtml-justify']
  1996. },
  1997. isInline: false,
  1998. format: '[justify]{0}[/justify]',
  1999. html: '<div align="justify">{0}</div>'
  2000. },
  2001. // END_COMMAND
  2002. // START_COMMAND: YouTube
  2003. youtube: {
  2004. allowsEmpty: true,
  2005. tags: {
  2006. iframe: {
  2007. 'data-youtube-id': null
  2008. }
  2009. },
  2010. format: function(element, content) {
  2011. element = element.attr('data-youtube-id');
  2012. return element ? '[youtube]' + element + '[/youtube]' : content;
  2013. },
  2014. html: '<iframe width="560" height="315" src="http://www.youtube.com/embed/{0}?wmode=opaque' +
  2015. '" data-youtube-id="{0}" frameborder="0" allowfullscreen></iframe>'
  2016. },
  2017. // END_COMMAND
  2018. // START_COMMAND: Rtl
  2019. rtl: {
  2020. styles: {
  2021. 'direction': ['rtl']
  2022. },
  2023. format: '[rtl]{0}[/rtl]',
  2024. html: '<div style="direction: rtl">{0}</div>'
  2025. },
  2026. // END_COMMAND
  2027. // START_COMMAND: Ltr
  2028. ltr: {
  2029. styles: {
  2030. 'direction': ['ltr']
  2031. },
  2032. format: '[ltr]{0}[/ltr]',
  2033. html: '<div style="direction: ltr">{0}</div>'
  2034. },
  2035. // END_COMMAND
  2036. // this is here so that commands above can be removed
  2037. // without having to remove the , after the last one.
  2038. // Needed for IE.
  2039. ignore: {}
  2040. };
  2041. /**
  2042. * Static BBCode helper class
  2043. * @class command
  2044. * @name jQuery.plugins.bbcode.bbcode
  2045. */
  2046. $.sceditor.plugins.bbcode.bbcode =
  2047. /** @lends jQuery.plugins.bbcode.bbcode */
  2048. {
  2049. /**
  2050. * Gets a BBCode
  2051. *
  2052. * @param {String} name
  2053. * @return {Object|null}
  2054. * @since v1.3.5
  2055. */
  2056. get: function(name) {
  2057. return $.sceditor.plugins.bbcode.bbcodes[name] || null;
  2058. },
  2059. /**
  2060. * <p>Adds a BBCode to the parser or updates an existing
  2061. * BBCode if a BBCode with the specified name already exists.</p>
  2062. *
  2063. * @param {String} name
  2064. * @param {Object} bbcode
  2065. * @return {this|false} Returns false if name or bbcode is false
  2066. * @since v1.3.5
  2067. */
  2068. set: function(name, bbcode) {
  2069. if(!name || !bbcode)
  2070. return false;
  2071. // merge any existing command properties
  2072. bbcode = $.extend($.sceditor.plugins.bbcode.bbcodes[name] || {}, bbcode);
  2073. bbcode.remove = function() { $.sceditor.plugins.bbcode.bbcode.remove(name); };
  2074. $.sceditor.plugins.bbcode.bbcodes[name] = bbcode;
  2075. return this;
  2076. },
  2077. /**
  2078. * Renames a BBCode
  2079. *
  2080. * This does not change the format or HTML handling, those must be
  2081. * changed manually.
  2082. *
  2083. * @param {String} name [description]
  2084. * @param {String} newName [description]
  2085. * @return {this|false}
  2086. * @since v1.4.0
  2087. */
  2088. rename: function(name, newName) {
  2089. if (this.hasOwnProperty(name))
  2090. {
  2091. this[newName] = this[name];
  2092. this.remove(name);
  2093. }
  2094. else
  2095. return false;
  2096. return this;
  2097. },
  2098. /**
  2099. * Removes a BBCode
  2100. *
  2101. * @param {String} name
  2102. * @return {this}
  2103. * @since v1.3.5
  2104. */
  2105. remove: function(name) {
  2106. if($.sceditor.plugins.bbcode.bbcodes[name])
  2107. delete $.sceditor.plugins.bbcode.bbcodes[name];
  2108. return this;
  2109. }
  2110. };
  2111. /**
  2112. * Deprecated, use plugins: option instead. I.e.:
  2113. *
  2114. * $('textarea').sceditor({
  2115. * plugins: 'bbcode'
  2116. * });
  2117. *
  2118. * @deprecated
  2119. */
  2120. $.fn.sceditorBBCodePlugin = function (options) {
  2121. options = options || {};
  2122. if($.isPlainObject(options))
  2123. options.plugins = (options.plugins ? options.plugins : '') + 'bbcode' ;
  2124. return this.sceditor(options);
  2125. };
  2126. })(jQuery, window, document);