JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
vanilla ckeditor-3.6.6.1
[ckeditor.git] / _source / plugins / bbcode / plugin.js
1 /*\r
2 Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
3 For licensing, see LICENSE.html or http://ckeditor.com/license\r
4 */\r
5 \r
6 (function()\r
7 {\r
8         CKEDITOR.on( 'dialogDefinition', function( ev )\r
9         {\r
10                 var tab, name = ev.data.name,\r
11                         definition = ev.data.definition;\r
12 \r
13                 if ( name == 'link' )\r
14                 {\r
15                         definition.removeContents( 'target' );\r
16                         definition.removeContents( 'upload' );\r
17                         definition.removeContents( 'advanced' );\r
18                         tab = definition.getContents( 'info' );\r
19                         tab.remove( 'emailSubject' );\r
20                         tab.remove( 'emailBody' );\r
21                 }\r
22                 else if ( name == 'image' )\r
23                 {\r
24                         definition.removeContents( 'advanced' );\r
25                         tab = definition.getContents( 'Link' );\r
26                         tab.remove( 'cmbTarget' );\r
27                         tab = definition.getContents( 'info' );\r
28                         tab.remove( 'txtAlt' );\r
29                         tab.remove( 'basic' );\r
30                 }\r
31         });\r
32 \r
33         var bbcodeMap = { 'b' : 'strong', 'u': 'u', 'i' : 'em', 'color' : 'span', 'size' : 'span', 'quote' : 'blockquote', 'code' : 'code', 'url' : 'a', 'email' : 'span', 'img' : 'span', '*' : 'li', 'list' : 'ol' },\r
34                         convertMap = { 'strong' : 'b' , 'b' : 'b', 'u': 'u', 'em' : 'i', 'i': 'i', 'code' : 'code', 'li' : '*' },\r
35                         tagnameMap = { 'strong' : 'b', 'em' : 'i', 'u' : 'u', 'li' : '*', 'ul' : 'list', 'ol' : 'list', 'code' : 'code', 'a' : 'link', 'img' : 'img', 'blockquote' : 'quote' },\r
36                         stylesMap = { 'color' : 'color', 'size' : 'font-size' },\r
37                         attributesMap = { 'url' : 'href', 'email' : 'mailhref', 'quote': 'cite', 'list' : 'listType' };\r
38 \r
39         // List of block-like tags.\r
40         var dtd =  CKEDITOR.dtd,\r
41                 blockLikeTags = CKEDITOR.tools.extend( { table:1 }, dtd.$block, dtd.$listItem, dtd.$tableContent, dtd.$list );\r
42 \r
43         var semicolonFixRegex = /\s*(?:;\s*|$)/;\r
44         function serializeStyleText( stylesObject )\r
45         {\r
46                 var styleText = '';\r
47                 for ( var style in stylesObject )\r
48                 {\r
49                         var styleVal = stylesObject[ style ],\r
50                                 text = ( style + ':' + styleVal ).replace( semicolonFixRegex, ';' );\r
51 \r
52                         styleText += text;\r
53                 }\r
54                 return styleText;\r
55         }\r
56 \r
57         function parseStyleText( styleText )\r
58         {\r
59                 var retval = {};\r
60                 ( styleText || '' )\r
61                                 .replace( /"/g, '"' )\r
62                                 .replace( /\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function( match, name, value )\r
63                 {\r
64                         retval[ name.toLowerCase() ] = value;\r
65                 } );\r
66                 return retval;\r
67         }\r
68 \r
69         function RGBToHex( cssStyle )\r
70         {\r
71                 return cssStyle.replace( /(?:rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\))/gi, function( match, red, green, blue )\r
72                         {\r
73                                 red = parseInt( red, 10 ).toString( 16 );\r
74                                 green = parseInt( green, 10 ).toString( 16 );\r
75                                 blue = parseInt( blue, 10 ).toString( 16 );\r
76                                 var color = [red, green, blue] ;\r
77 \r
78                                 // Add padding zeros if the hex value is less than 0x10.\r
79                                 for ( var i = 0 ; i < color.length ; i++ )\r
80                                         color[i] = String( '0' + color[i] ).slice( -2 ) ;\r
81 \r
82                                 return '#' + color.join( '' ) ;\r
83                          });\r
84         }\r
85 \r
86         // Maintain the map of smiley-to-description.\r
87         var smileyMap = {"smiley":":)","sad":":(","wink":";)","laugh":":D","cheeky":":P","blush":":*)","surprise":":-o","indecision":":|","angry":">:(","angel":"o:)","cool":"8-)","devil":">:-)","crying":";(","kiss":":-*" },\r
88                 smileyReverseMap = {},\r
89                 smileyRegExp = [];\r
90 \r
91         // Build regexp for the list of smiley text.\r
92         for ( var i in smileyMap )\r
93         {\r
94                 smileyReverseMap[ smileyMap[ i ] ] = i;\r
95                 smileyRegExp.push( smileyMap[ i ].replace( /\(|\)|\:|\/|\*|\-|\|/g, function( match ) { return '\\' + match; } ) );\r
96         }\r
97 \r
98         smileyRegExp = new RegExp( smileyRegExp.join( '|' ), 'g' );\r
99 \r
100         var decodeHtml = ( function ()\r
101         {\r
102                 var regex = [],\r
103                         entities =\r
104                         {\r
105                                 nbsp    : '\u00A0',             // IE | FF\r
106                                 shy             : '\u00AD',             // IE\r
107                                 gt              : '\u003E',             // IE | FF |   --   | Opera\r
108                                 lt              : '\u003C'              // IE | FF | Safari | Opera\r
109                         };\r
110 \r
111                 for ( var entity in entities )\r
112                         regex.push( entity );\r
113 \r
114                 regex = new RegExp( '&(' + regex.join( '|' ) + ');', 'g' );\r
115 \r
116                 return function( html )\r
117                 {\r
118                         return html.replace( regex, function( match, entity )\r
119                         {\r
120                                 return entities[ entity ];\r
121                         });\r
122                 };\r
123         })();\r
124 \r
125         CKEDITOR.BBCodeParser = function()\r
126         {\r
127                 this._ =\r
128                 {\r
129                         bbcPartsRegex : /(?:\[([^\/\]=]*?)(?:=([^\]]*?))?\])|(?:\[\/([a-z]{1,16})\])/ig\r
130                 };\r
131         };\r
132 \r
133         CKEDITOR.BBCodeParser.prototype =\r
134         {\r
135                 parse : function( bbcode )\r
136                 {\r
137                         var parts,\r
138                                         part,\r
139                                         lastIndex = 0;\r
140 \r
141                         while ( ( parts = this._.bbcPartsRegex.exec( bbcode ) ) )\r
142                         {\r
143                                 var tagIndex = parts.index;\r
144                                 if ( tagIndex > lastIndex )\r
145                                 {\r
146                                         var text = bbcode.substring( lastIndex, tagIndex );\r
147                                         this.onText( text, 1 );\r
148                                 }\r
149 \r
150                                 lastIndex = this._.bbcPartsRegex.lastIndex;\r
151 \r
152                                 /*\r
153                                  "parts" is an array with the following items:\r
154                                  0 : The entire match for opening/closing tags and line-break;\r
155                                  1 : line-break;\r
156                                  2 : open of tag excludes option;\r
157                                  3 : tag option;\r
158                                  4 : close of tag;\r
159                                  */\r
160 \r
161                                 part = ( parts[ 1 ] || parts[ 3 ] || '' ).toLowerCase();\r
162                                 // Unrecognized tags should be delivered as a simple text (#7860).\r
163                                 if ( part && !bbcodeMap[ part ] )\r
164                                 {\r
165                                         this.onText( parts[ 0 ] );\r
166                                         continue;\r
167                                 }\r
168 \r
169                                 // Opening tag\r
170                                 if ( parts[ 1 ] )\r
171                                 {\r
172                                         var tagName = bbcodeMap[ part ],\r
173                                                         attribs = {},\r
174                                                         styles = {},\r
175                                                         optionPart = parts[ 2 ];\r
176 \r
177                                         if ( optionPart )\r
178                                         {\r
179                                                 if ( part == 'list' )\r
180                                                 {\r
181                                                         if ( !isNaN( optionPart ) )\r
182                                                                 optionPart = 'decimal';\r
183                                                         else if ( /^[a-z]+$/.test( optionPart ) )\r
184                                                                 optionPart = 'lower-alpha';\r
185                                                         else if ( /^[A-Z]+$/.test( optionPart ) )\r
186                                                                 optionPart = 'upper-alpha';\r
187                                                 }\r
188 \r
189                                                 if ( stylesMap[ part ] )\r
190                                                 {\r
191                                                         // Font size represents percentage.\r
192                                                         if ( part == 'size' )\r
193                                                                 optionPart += '%';\r
194 \r
195                                                         styles[ stylesMap[ part ] ] = optionPart;\r
196                                                         attribs.style = serializeStyleText( styles );\r
197                                                 }\r
198                                                 else if ( attributesMap[ part ] )\r
199                                                         attribs[ attributesMap[ part ] ] = optionPart;\r
200                                         }\r
201 \r
202                                         // Two special handling - image and email, protect them\r
203                                         // as "span" with an attribute marker.\r
204                                         if ( part == 'email' || part == 'img' )\r
205                                                 attribs[ 'bbcode' ] = part;\r
206 \r
207                                         this.onTagOpen( tagName, attribs, CKEDITOR.dtd.$empty[ tagName ] );\r
208                                 }\r
209                                 // Closing tag\r
210                                 else if ( parts[ 3 ] )\r
211                                         this.onTagClose( bbcodeMap[ part ] );\r
212                         }\r
213 \r
214                         if ( bbcode.length > lastIndex )\r
215                                 this.onText( bbcode.substring( lastIndex, bbcode.length ), 1 );\r
216                 }\r
217         };\r
218 \r
219         /**\r
220          * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string.\r
221          * @param {String} source The HTML to be parsed, filling the fragment.\r
222          * @param {Number} [fixForBody=false] Wrap body with specified element if needed.\r
223          * @returns CKEDITOR.htmlParser.fragment The fragment created.\r
224          * @example\r
225          * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' );\r
226          * alert( fragment.children[0].name );  "b"\r
227          * alert( fragment.children[1].value );  " Text"\r
228          */\r
229         CKEDITOR.htmlParser.fragment.fromBBCode = function( source )\r
230         {\r
231                 var parser = new CKEDITOR.BBCodeParser(),\r
232                         fragment = new CKEDITOR.htmlParser.fragment(),\r
233                         pendingInline = [],\r
234                         pendingBrs = 0,\r
235                         currentNode = fragment,\r
236                         returnPoint;\r
237 \r
238                 function checkPending( newTagName )\r
239                 {\r
240                         if ( pendingInline.length > 0 )\r
241                         {\r
242                                 for ( var i = 0 ; i < pendingInline.length ; i++ )\r
243                                 {\r
244                                         var pendingElement = pendingInline[ i ],\r
245                                                 pendingName = pendingElement.name,\r
246                                                 pendingDtd = CKEDITOR.dtd[ pendingName ],\r
247                                                 currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ];\r
248 \r
249                                         if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) )\r
250                                         {\r
251                                                 // Get a clone for the pending element.\r
252                                                 pendingElement = pendingElement.clone();\r
253 \r
254                                                 // Add it to the current node and make it the current,\r
255                                                 // so the new element will be added inside of it.\r
256                                                 pendingElement.parent = currentNode;\r
257                                                 currentNode = pendingElement;\r
258 \r
259                                                 // Remove the pending element (back the index by one\r
260                                                 // to properly process the next entry).\r
261                                                 pendingInline.splice( i, 1 );\r
262                                                 i--;\r
263                                         }\r
264                                 }\r
265                         }\r
266                 }\r
267 \r
268                 function checkPendingBrs( tagName, closing )\r
269                 {\r
270                         var len = currentNode.children.length,\r
271                                 previous = len > 0 && currentNode.children[ len - 1 ],\r
272                                 lineBreakParent = !previous && BBCodeWriter.getRule( tagnameMap[ currentNode.name ], 'breakAfterOpen' ),\r
273                                 lineBreakPrevious = previous && previous.type == CKEDITOR.NODE_ELEMENT && BBCodeWriter.getRule( tagnameMap[ previous.name ], 'breakAfterClose' ),\r
274                                 lineBreakCurrent = tagName && BBCodeWriter.getRule( tagnameMap[ tagName ], closing ? 'breakBeforeClose' : 'breakBeforeOpen' );\r
275 \r
276                         if ( pendingBrs && ( lineBreakParent || lineBreakPrevious || lineBreakCurrent ) )\r
277                                 pendingBrs--;\r
278 \r
279                         // 1. Either we're at the end of block, where it requires us to compensate the br filler\r
280                         // removing logic (from htmldataprocessor).\r
281                         // 2. Or we're at the end of pseudo block, where it requires us to compensate\r
282                         // the bogus br effect.\r
283                         if ( pendingBrs && tagName in blockLikeTags )\r
284                                 pendingBrs++;\r
285 \r
286                         while ( pendingBrs && pendingBrs-- )\r
287                                 currentNode.children.push( previous = new CKEDITOR.htmlParser.element( 'br' ) );\r
288                 }\r
289 \r
290                 function addElement( node, target )\r
291                 {\r
292                         checkPendingBrs( node.name, 1 );\r
293 \r
294                         target = target || currentNode || fragment;\r
295 \r
296                         var len = target.children.length,\r
297                                 previous = len > 0 && target.children[ len - 1 ] || null;\r
298 \r
299                         node.previous = previous;\r
300                         node.parent = target;\r
301 \r
302                         target.children.push( node );\r
303 \r
304                         if ( node.returnPoint )\r
305                         {\r
306                                 currentNode = node.returnPoint;\r
307                                 delete node.returnPoint;\r
308                         }\r
309                 }\r
310 \r
311                 parser.onTagOpen = function( tagName, attributes, selfClosing )\r
312                 {\r
313                         var element = new CKEDITOR.htmlParser.element( tagName, attributes );\r
314 \r
315                         // This is a tag to be removed if empty, so do not add it immediately.\r
316                         if ( CKEDITOR.dtd.$removeEmpty[ tagName ] )\r
317                         {\r
318                                 pendingInline.push( element );\r
319                                 return;\r
320                         }\r
321 \r
322                         var currentName = currentNode.name;\r
323 \r
324                         var currentDtd = currentName\r
325                                 && ( CKEDITOR.dtd[ currentName ]\r
326                                         || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) );\r
327 \r
328                         // If the element cannot be child of the current element.\r
329                         if ( currentDtd && !currentDtd[ tagName ] )\r
330                         {\r
331                                 var reApply = false,\r
332                                         addPoint;   // New position to start adding nodes.\r
333 \r
334                                 // If the element name is the same as the current element name,\r
335                                 // then just close the current one and append the new one to the\r
336                                 // parent. This situation usually happens with <p>, <li>, <dt> and\r
337                                 // <dd>, specially in IE. Do not enter in this if block in this case.\r
338                                 if ( tagName == currentName )\r
339                                         addElement( currentNode, currentNode.parent );\r
340                                 else if ( tagName in CKEDITOR.dtd.$listItem )\r
341                                 {\r
342                                         parser.onTagOpen( 'ul', {} );\r
343                                         addPoint = currentNode;\r
344                                         reApply = true;\r
345                                 }\r
346                                 else\r
347                                 {\r
348                                         addElement( currentNode, currentNode.parent );\r
349 \r
350                                         // The current element is an inline element, which\r
351                                         // cannot hold the new one. Put it in the pending list,\r
352                                         // and try adding the new one after it.\r
353                                         pendingInline.unshift( currentNode );\r
354                                         reApply = true;\r
355                                 }\r
356 \r
357                                 if ( addPoint )\r
358                                         currentNode = addPoint;\r
359                                 // Try adding it to the return point, or the parent element.\r
360                                 else\r
361                                         currentNode = currentNode.returnPoint || currentNode.parent;\r
362 \r
363                                 if ( reApply )\r
364                                 {\r
365                                         parser.onTagOpen.apply( this, arguments );\r
366                                         return;\r
367                                 }\r
368                         }\r
369 \r
370                         checkPending( tagName );\r
371                         checkPendingBrs( tagName );\r
372 \r
373                         element.parent = currentNode;\r
374                         element.returnPoint = returnPoint;\r
375                         returnPoint = 0;\r
376 \r
377                         if ( element.isEmpty )\r
378                                 addElement( element );\r
379                         else\r
380                                 currentNode = element;\r
381                 };\r
382 \r
383                 parser.onTagClose = function( tagName )\r
384                 {\r
385                         // Check if there is any pending tag to be closed.\r
386                         for ( var i = pendingInline.length - 1 ; i >= 0 ; i-- )\r
387                         {\r
388                                 // If found, just remove it from the list.\r
389                                 if ( tagName == pendingInline[ i ].name )\r
390                                 {\r
391                                         pendingInline.splice( i, 1 );\r
392                                         return;\r
393                                 }\r
394                         }\r
395 \r
396                         var pendingAdd = [],\r
397                                 newPendingInline = [],\r
398                                 candidate = currentNode;\r
399 \r
400                         while ( candidate.type && candidate.name != tagName )\r
401                         {\r
402                                 // If this is an inline element, add it to the pending list, if we're\r
403                                 // really closing one of the parents element later, they will continue\r
404                                 // after it.\r
405                                 if ( !candidate._.isBlockLike )\r
406                                         newPendingInline.unshift( candidate );\r
407 \r
408                                 // This node should be added to it's parent at this point. But,\r
409                                 // it should happen only if the closing tag is really closing\r
410                                 // one of the nodes. So, for now, we just cache it.\r
411                                 pendingAdd.push( candidate );\r
412 \r
413                                 candidate = candidate.parent;\r
414                         }\r
415 \r
416                         if ( candidate.type )\r
417                         {\r
418                                 // Add all elements that have been found in the above loop.\r
419                                 for ( i = 0 ; i < pendingAdd.length ; i++ )\r
420                                 {\r
421                                         var node = pendingAdd[ i ];\r
422                                         addElement( node, node.parent );\r
423                                 }\r
424 \r
425                                 currentNode = candidate;\r
426 \r
427 \r
428                                 addElement( candidate, candidate.parent );\r
429 \r
430                                 // The parent should start receiving new nodes now, except if\r
431                                 // addElement changed the currentNode.\r
432                                 if ( candidate == currentNode )\r
433                                         currentNode = currentNode.parent;\r
434 \r
435                                 pendingInline = pendingInline.concat( newPendingInline );\r
436                         }\r
437                 };\r
438 \r
439                 parser.onText = function( text )\r
440                 {\r
441                         var currentDtd = CKEDITOR.dtd[ currentNode.name ];\r
442                         if ( !currentDtd || currentDtd[ '#' ] )\r
443                         {\r
444                                 checkPendingBrs();\r
445                                 checkPending();\r
446 \r
447                                 text.replace(/([\r\n])|[^\r\n]*/g, function( piece, lineBreak )\r
448                                 {\r
449                                         if ( lineBreak !== undefined && lineBreak.length )\r
450                                                 pendingBrs++;\r
451                                         else if ( piece.length )\r
452                                         {\r
453                                                 var lastIndex = 0;\r
454 \r
455                                                 // Create smiley from text emotion.\r
456                                                 piece.replace( smileyRegExp, function( match, index )\r
457                                                 {\r
458                                                         addElement( new CKEDITOR.htmlParser.text( piece.substring( lastIndex, index ) ), currentNode );\r
459                                                         addElement( new CKEDITOR.htmlParser.element( 'smiley', { 'desc': smileyReverseMap[ match ] } ), currentNode );\r
460                                                         lastIndex = index + match.length;\r
461                                                 });\r
462 \r
463                                                 if ( lastIndex != piece.length )\r
464                                                         addElement( new CKEDITOR.htmlParser.text( piece.substring( lastIndex, piece.length ) ), currentNode );\r
465                                         }\r
466                                 });\r
467                         }\r
468                 };\r
469 \r
470                 // Parse it.\r
471                 parser.parse( CKEDITOR.tools.htmlEncode( source ) );\r
472 \r
473                 // Close all hanging nodes.\r
474                 while ( currentNode.type )\r
475                 {\r
476                         var parent = currentNode.parent,\r
477                                 node = currentNode;\r
478 \r
479                         addElement( node, parent );\r
480                         currentNode = parent;\r
481                 }\r
482 \r
483                 return fragment;\r
484         };\r
485 \r
486         CKEDITOR.htmlParser.BBCodeWriter = CKEDITOR.tools.createClass(\r
487         {\r
488                 $ : function()\r
489                 {\r
490                         this._ =\r
491                         {\r
492                                 output : [],\r
493                                 rules : []\r
494                         };\r
495 \r
496                         // List and list item.\r
497                         this.setRules( 'list',\r
498                    {\r
499                            breakBeforeOpen : 1,\r
500                            breakAfterOpen : 1,\r
501                            breakBeforeClose : 1,\r
502                            breakAfterClose : 1\r
503                    } );\r
504 \r
505                         this.setRules( '*',\r
506                    {\r
507                            breakBeforeOpen : 1,\r
508                            breakAfterOpen : 0,\r
509                            breakBeforeClose : 1,\r
510                            breakAfterClose : 0\r
511                    } );\r
512 \r
513                         this.setRules( 'quote',\r
514                    {\r
515                            breakBeforeOpen : 1,\r
516                            breakAfterOpen : 0,\r
517                            breakBeforeClose : 0,\r
518                            breakAfterClose : 1\r
519                    } );\r
520                 },\r
521 \r
522                 proto :\r
523                 {\r
524                         /**\r
525                          * Sets formatting rules for a given tag. The possible rules are:\r
526                          * <ul>\r
527                          *      <li><b>breakBeforeOpen</b>: break line before the opener tag for this element.</li>\r
528                          *      <li><b>breakAfterOpen</b>: break line after the opener tag for this element.</li>\r
529                          *      <li><b>breakBeforeClose</b>: break line before the closer tag for this element.</li>\r
530                          *      <li><b>breakAfterClose</b>: break line after the closer tag for this element.</li>\r
531                          * </ul>\r
532                          *\r
533                          * All rules default to "false". Each call to the function overrides\r
534                          * already present rules, leaving the undefined untouched.\r
535                          *\r
536                          * @param {String} tagName The tag name to which set the rules.\r
537                          * @param {Object} rules An object containing the element rules.\r
538                          * @example\r
539                          * // Break line before and after "img" tags.\r
540                          * writer.setRules( 'list',\r
541                          *     {\r
542                          *         breakBeforeOpen : true\r
543                          *         breakAfterOpen : true\r
544                          *     });\r
545                          */\r
546                         setRules : function( tagName, rules )\r
547                         {\r
548                                 var currentRules = this._.rules[ tagName ];\r
549 \r
550                                 if ( currentRules )\r
551                                         CKEDITOR.tools.extend( currentRules, rules, true );\r
552                                 else\r
553                                         this._.rules[ tagName ] = rules;\r
554                         },\r
555 \r
556                         getRule : function( tagName, ruleName )\r
557                         {\r
558                                 return this._.rules[ tagName ] && this._.rules[ tagName ][ ruleName ];\r
559                         },\r
560 \r
561                         openTag : function( tag, attributes )\r
562                         {\r
563                                 if ( tag in bbcodeMap )\r
564                                 {\r
565                                         if ( this.getRule( tag, 'breakBeforeOpen' ) )\r
566                                                 this.lineBreak( 1 );\r
567 \r
568                                         this.write( '[', tag );\r
569                                 }\r
570                         },\r
571 \r
572                         openTagClose : function( tag )\r
573                         {\r
574 \r
575                                 if ( tag == 'br' )\r
576                                         this._.output.push( '\n' );\r
577                                 else if ( tag in bbcodeMap )\r
578                                 {\r
579                                         this.write( ']' );\r
580                                         if ( this.getRule( tag, 'breakAfterOpen' ) )\r
581                                                 this.lineBreak( 1 );\r
582                                 }\r
583                         },\r
584 \r
585                         attribute : function( name, val )\r
586                         {\r
587                                 if ( name == 'option' )\r
588                                 {\r
589                                         // Force simply ampersand in attributes.\r
590                                         if ( typeof val == 'string' )\r
591                                                 val = val.replace( /&amp;/g, '&' );\r
592 \r
593                                         this.write( '=', val );\r
594                                 }\r
595                         },\r
596 \r
597                         closeTag : function( tag )\r
598                         {\r
599                                 if ( tag in bbcodeMap )\r
600                                 {\r
601                                         if ( this.getRule( tag, 'breakBeforeClose' ) )\r
602                                                 this.lineBreak( 1 );\r
603 \r
604                                         tag != '*' && this.write( '[/', tag, ']' );\r
605 \r
606                                         if ( this.getRule( tag, 'breakAfterClose' ) )\r
607                                                 this.lineBreak( 1 );\r
608                                 }\r
609                         },\r
610 \r
611                         text : function( text )\r
612                         {\r
613                                 this.write( text );\r
614                         },\r
615 \r
616                         /**\r
617                          * Writes a comment.\r
618                          * @param {String} comment The comment text.\r
619                          * @example\r
620                          * // Writes "&lt;!-- My comment --&gt;".\r
621                          * writer.comment( ' My comment ' );\r
622                          */\r
623                         comment : function() {},\r
624 \r
625                         /*\r
626                         * Output line-break for formatting.\r
627                          */\r
628                         lineBreak : function()\r
629                         {\r
630                                 // Avoid line break when:\r
631                                 // 1) Previous tag already put one.\r
632                                 // 2) We're at output start.\r
633                                 if ( !this._.hasLineBreak && this._.output.length )\r
634                                 {\r
635                                         this.write( '\n' );\r
636                                         this._.hasLineBreak = 1;\r
637                                 }\r
638                         },\r
639 \r
640                         write : function()\r
641                         {\r
642                                 this._.hasLineBreak = 0;\r
643                                 var data = Array.prototype.join.call( arguments, '' );\r
644                                 this._.output.push( data );\r
645                         },\r
646 \r
647                         reset : function()\r
648                         {\r
649                                 this._.output = [];\r
650                                 this._.hasLineBreak = 0;\r
651                         },\r
652 \r
653                         getHtml : function( reset )\r
654                         {\r
655                                 var bbcode = this._.output.join( '' );\r
656 \r
657                                 if ( reset )\r
658                                         this.reset();\r
659 \r
660                                 return decodeHtml ( bbcode );\r
661                         }\r
662                 }\r
663         });\r
664 \r
665         var BBCodeWriter = new CKEDITOR.htmlParser.BBCodeWriter();\r
666 \r
667         CKEDITOR.plugins.add( 'bbcode',\r
668           {\r
669                   requires : [ 'htmldataprocessor', 'entities' ],\r
670                   beforeInit : function( editor )\r
671                   {\r
672                           // Adapt some critical editor configuration for better support\r
673                           // of BBCode environment.\r
674                           var config = editor.config;\r
675                           CKEDITOR.tools.extend( config,\r
676                           {\r
677                                   enterMode : CKEDITOR.ENTER_BR,\r
678                                   basicEntities: false,\r
679                                   entities : false,\r
680                                   fillEmptyBlocks : false\r
681                           }, true );\r
682                   },\r
683                   init : function( editor )\r
684                   {\r
685                           var config = editor.config;\r
686 \r
687                           function BBCodeToHtml( code )\r
688                           {\r
689                                   var fragment = CKEDITOR.htmlParser.fragment.fromBBCode( code ),\r
690                                                   writer = new CKEDITOR.htmlParser.basicWriter();\r
691 \r
692                                   fragment.writeHtml( writer, dataFilter );\r
693                                   return writer.getHtml( true );\r
694                           }\r
695 \r
696                           var dataFilter = new CKEDITOR.htmlParser.filter();\r
697                           dataFilter.addRules(\r
698                           {\r
699                                   elements :\r
700                                   {\r
701                                           'blockquote' : function( element )\r
702                                           {\r
703                                                   var quoted = new CKEDITOR.htmlParser.element( 'div' );\r
704                                                   quoted.children = element.children;\r
705                                                   element.children = [ quoted ];\r
706                                                   var citeText = element.attributes.cite;\r
707                                                   if ( citeText )\r
708                                                   {\r
709                                                           var cite = new CKEDITOR.htmlParser.element( 'cite' );\r
710                                                           cite.add( new CKEDITOR.htmlParser.text( citeText.replace( /^"|"$/g, '' ) ) );\r
711                                                           delete element.attributes.cite;\r
712                                                           element.children.unshift( cite );\r
713                                                   }\r
714                                           },\r
715                                           'span' : function( element )\r
716                                           {\r
717                                                   var bbcode;\r
718                                                   if ( ( bbcode = element.attributes.bbcode ) )\r
719                                                   {\r
720                                                           if ( bbcode == 'img' )\r
721                                                           {\r
722                                                                   element.name = 'img';\r
723                                                                   element.attributes.src = element.children[ 0 ].value;\r
724                                                                   element.children = [];\r
725                                                           }\r
726                                                           else if ( bbcode == 'email' )\r
727                                                           {\r
728                                                                   element.name = 'a';\r
729                                                                   element.attributes.href = 'mailto:' + element.children[ 0 ].value;\r
730                                                           }\r
731 \r
732                                                           delete element.attributes.bbcode;\r
733                                                   }\r
734                                           },\r
735                                           'ol' : function ( element )\r
736                                           {\r
737                                                   if ( element.attributes.listType )\r
738                                                   {\r
739                                                           if ( element.attributes.listType != 'decimal' )\r
740                                                                   element.attributes.style = 'list-style-type:' + element.attributes.listType;\r
741                                                   }\r
742                                                   else\r
743                                                           element.name = 'ul';\r
744 \r
745                                                   delete element.attributes.listType;\r
746                                           },\r
747                                           a : function( element )\r
748                                           {\r
749                                                   if ( !element.attributes.href )\r
750                                                           element.attributes.href = element.children[ 0 ].value;\r
751                                           },\r
752                                           'smiley' : function( element )\r
753                                           {\r
754                                                         element.name = 'img';\r
755 \r
756                                                         var description = element.attributes.desc,\r
757                                                                 image = config.smiley_images[ CKEDITOR.tools.indexOf( config.smiley_descriptions, description ) ],\r
758                                                                 src = CKEDITOR.tools.htmlEncode( config.smiley_path + image );\r
759 \r
760                                                   element.attributes =\r
761                                                   {\r
762                                                           src : src,\r
763                                                           'data-cke-saved-src' : src,\r
764                                                           title :  description,\r
765                                                           alt : description\r
766                                                   };\r
767                                           }\r
768                                   }\r
769                           } );\r
770 \r
771                           editor.dataProcessor.htmlFilter.addRules(\r
772                           {\r
773                                 elements :\r
774                                 {\r
775                                         $ : function( element )\r
776                                         {\r
777                                                 var attributes = element.attributes,\r
778                                                                 style = parseStyleText( attributes.style ),\r
779                                                                 value;\r
780 \r
781                                                 var tagName = element.name;\r
782                                                 if ( tagName in convertMap )\r
783                                                         tagName = convertMap[ tagName ];\r
784                                                 else if ( tagName == 'span' )\r
785                                                 {\r
786                                                         if ( ( value = style.color ) )\r
787                                                         {\r
788                                                                 tagName = 'color';\r
789                                                                 value = RGBToHex( value );\r
790                                                         }\r
791                                                         else if ( ( value = style[ 'font-size' ] ) )\r
792                                                         {\r
793                                                                 var percentValue = value.match( /(\d+)%$/ );\r
794                                                                 if ( percentValue )\r
795                                                                 {\r
796                                                                         value = percentValue[ 1 ];\r
797                                                                         tagName = 'size';\r
798                                                                 }\r
799                                                         }\r
800                                                 }\r
801                                                 else if ( tagName == 'ol' || tagName == 'ul' )\r
802                                                 {\r
803                                                         if ( ( value = style[ 'list-style-type'] ) )\r
804                                                         {\r
805                                                                 switch ( value )\r
806                                                                 {\r
807                                                                         case 'lower-alpha':\r
808                                                                                 value = 'a';\r
809                                                                                 break;\r
810                                                                         case 'upper-alpha':\r
811                                                                                 value = 'A';\r
812                                                                                 break;\r
813                                                                 }\r
814                                                         }\r
815                                                         else if ( tagName == 'ol' )\r
816                                                                 value = 1;\r
817 \r
818                                                         tagName = 'list';\r
819                                                 }\r
820                                                 else if ( tagName == 'blockquote' )\r
821                                                 {\r
822                                                         try\r
823                                                         {\r
824                                                                 var cite = element.children[ 0 ],\r
825                                                                                 quoted = element.children[ 1 ],\r
826                                                                                 citeText = cite.name == 'cite' && cite.children[ 0 ].value;\r
827 \r
828                                                                 if ( citeText )\r
829                                                                 {\r
830                                                                         value = '"' + citeText + '"';\r
831                                                                         element.children = quoted.children;\r
832                                                                 }\r
833 \r
834                                                         }\r
835                                                         catch( er )\r
836                                                         {\r
837                                                         }\r
838 \r
839                                                         tagName = 'quote';\r
840                                                 }\r
841                                                 else if ( tagName == 'a' )\r
842                                                 {\r
843                                                         if ( ( value = attributes.href ) )\r
844                                                         {\r
845                                                                 if ( value.indexOf( 'mailto:' ) !== -1 )\r
846                                                                 {\r
847                                                                         tagName = 'email';\r
848                                                                         // [email] should have a single text child with email address.\r
849                                                                         element.children = [ new CKEDITOR.htmlParser.text( value.replace( 'mailto:', '' ) ) ];\r
850                                                                         value = '';\r
851                                                                 }\r
852                                                                 else\r
853                                                                 {\r
854                                                                         var singleton = element.children.length == 1 && element.children[ 0 ];\r
855                                                                         if ( singleton\r
856                                                                                         && singleton.type == CKEDITOR.NODE_TEXT\r
857                                                                                         && singleton.value == value )\r
858                                                                                 value = '';\r
859 \r
860                                                                         tagName = 'url';\r
861                                                                 }\r
862                                                         }\r
863                                                 }\r
864                                                 else if ( tagName == 'img' )\r
865                                                 {\r
866                                                         element.isEmpty = 0;\r
867 \r
868                                                         // Translate smiley (image) to text emotion.\r
869                                                         var src = attributes[ 'data-cke-saved-src' ];\r
870                                                         if ( src && src.indexOf( editor.config.smiley_path ) != -1 )\r
871                                                                 return new CKEDITOR.htmlParser.text( smileyMap[ attributes.alt ] );\r
872                                                         else\r
873                                                                 element.children = [ new CKEDITOR.htmlParser.text( src ) ];\r
874                                                 }\r
875 \r
876                                                 element.name = tagName;\r
877                                                 value && ( element.attributes.option = value );\r
878 \r
879                                                 return null;\r
880                                         },\r
881 \r
882                                         // Remove any bogus br from the end of a pseudo block,\r
883                                         // e.g. <div>some text<br /><p>paragraph</p></div>\r
884                                         br : function( element )\r
885                                         {\r
886                                                 var next = element.next;\r
887                                                 if ( next && next.name in blockLikeTags )\r
888                                                         return false;\r
889                                         }\r
890                                 }\r
891                           }, 1 );\r
892 \r
893                           editor.dataProcessor.writer = BBCodeWriter;\r
894 \r
895                           editor.on( 'beforeSetMode', function( evt )\r
896                           {\r
897                                   evt.removeListener();\r
898                                   var wysiwyg = editor._.modes[ 'wysiwyg' ];\r
899                                   wysiwyg.loadData = CKEDITOR.tools.override( wysiwyg.loadData, function( org )\r
900                                   {\r
901                                           return function( data )\r
902                                           {\r
903                                                   return ( org.call( this, BBCodeToHtml( data ) ) );\r
904                                           };\r
905                                   } );\r
906                           } );\r
907                   },\r
908 \r
909                   afterInit : function( editor )\r
910                   {\r
911                           var filters;\r
912                           if ( editor._.elementsPath  )\r
913                           {\r
914                                   // Eliminate irrelevant elements from displaying, e.g body and p.\r
915                                   if ( ( filters = editor._.elementsPath.filters ) )\r
916                                         filters.push( function( element )\r
917                                                 {\r
918                                                         var htmlName = element.getName(),\r
919                                                                 name = tagnameMap[ htmlName ] || false;\r
920 \r
921                                                         // Specialized anchor presents as email.\r
922                                                         if ( name == 'link' && element.getAttribute( 'href' ).indexOf( 'mailto:' ) === 0 )\r
923                                                                 name = 'email';\r
924                                                         // Styled span could be either size or color.\r
925                                                         else if ( htmlName == 'span' )\r
926                                                         {\r
927                                                                 if ( element.getStyle( 'font-size' ) )\r
928                                                                         name = 'size';\r
929                                                                 else if ( element.getStyle( 'color' ) )\r
930                                                                         name = 'color';\r
931                                                         }\r
932                                                         else if ( name == 'img' )\r
933                                                         {\r
934                                                                 var src = element.data( 'cke-saved-src' );\r
935                                                                 if ( src && src.indexOf( editor.config.smiley_path ) === 0 )\r
936                                                                         name = 'smiley';\r
937                                                         }\r
938 \r
939                                                         return name;\r
940                                                 });\r
941                           }\r
942                   }\r
943           } );\r
944 \r
945 })();\r