JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
vanilla ckeditor-3.6.1
[ckeditor.git] / _source / plugins / bbcode / plugin.js
1 /*\r
2 Copyright (c) 2003-2011, 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                                         var option = attributes.option;\r
570                                         option && this.write( '=', option );\r
571                                         this.write( ']' );\r
572 \r
573                                         if ( this.getRule( tag, 'breakAfterOpen' ) )\r
574                                                 this.lineBreak( 1 );\r
575                                 }\r
576                                 else if ( tag == 'br' )\r
577                                         this._.output.push( '\n' );\r
578                         },\r
579 \r
580                         openTagClose : function() { },\r
581                         attribute : function() { },\r
582 \r
583                         closeTag : function( tag )\r
584                         {\r
585                                 if ( tag in bbcodeMap )\r
586                                 {\r
587                                         if ( this.getRule( tag, 'breakBeforeClose' ) )\r
588                                                 this.lineBreak( 1 );\r
589 \r
590                                         tag != '*' && this.write( '[/', tag, ']' );\r
591 \r
592                                         if ( this.getRule( tag, 'breakAfterClose' ) )\r
593                                                 this.lineBreak( 1 );\r
594                                 }\r
595                         },\r
596 \r
597                         text : function( text )\r
598                         {\r
599                                 this.write( text );\r
600                         },\r
601 \r
602                         /**\r
603                          * Writes a comment.\r
604                          * @param {String} comment The comment text.\r
605                          * @example\r
606                          * // Writes "&lt;!-- My comment --&gt;".\r
607                          * writer.comment( ' My comment ' );\r
608                          */\r
609                         comment : function() {},\r
610 \r
611                         /*\r
612                         * Output line-break for formatting.\r
613                          */\r
614                         lineBreak : function()\r
615                         {\r
616                                 // Avoid line break when:\r
617                                 // 1) Previous tag already put one.\r
618                                 // 2) We're at output start.\r
619                                 if ( !this._.hasLineBreak && this._.output.length )\r
620                                 {\r
621                                         this.write( '\n' );\r
622                                         this._.hasLineBreak = 1;\r
623                                 }\r
624                         },\r
625 \r
626                         write : function()\r
627                         {\r
628                                 this._.hasLineBreak = 0;\r
629                                 var data = Array.prototype.join.call( arguments, '' );\r
630                                 this._.output.push( data );\r
631                         },\r
632 \r
633                         reset : function()\r
634                         {\r
635                                 this._.output = [];\r
636                                 this._.hasLineBreak = 0;\r
637                         },\r
638 \r
639                         getHtml : function( reset )\r
640                         {\r
641                                 var bbcode = this._.output.join( '' );\r
642 \r
643                                 if ( reset )\r
644                                         this.reset();\r
645 \r
646                                 return decodeHtml ( bbcode );\r
647                         }\r
648                 }\r
649         });\r
650 \r
651         var BBCodeWriter = new CKEDITOR.htmlParser.BBCodeWriter();\r
652 \r
653         CKEDITOR.plugins.add( 'bbcode',\r
654           {\r
655                   requires : [ 'htmldataprocessor', 'entities' ],\r
656                   beforeInit : function( editor )\r
657                   {\r
658                           // Adapt some critical editor configuration for better support\r
659                           // of BBCode environment.\r
660                           var config = editor.config;\r
661                           CKEDITOR.tools.extend( config,\r
662                           {\r
663                                   enterMode : CKEDITOR.ENTER_BR,\r
664                                   basicEntities: false,\r
665                                   entities : false,\r
666                                   fillEmptyBlocks : false\r
667                           }, true );\r
668                   },\r
669                   init : function( editor )\r
670                   {\r
671                           var config = editor.config;\r
672 \r
673                           function BBCodeToHtml( code )\r
674                           {\r
675                                   var fragment = CKEDITOR.htmlParser.fragment.fromBBCode( code ),\r
676                                                   writer = new CKEDITOR.htmlParser.basicWriter();\r
677 \r
678                                   fragment.writeHtml( writer, dataFilter );\r
679                                   return writer.getHtml( true );\r
680                           }\r
681 \r
682                           var dataFilter = new CKEDITOR.htmlParser.filter();\r
683                           dataFilter.addRules(\r
684                           {\r
685                                   elements :\r
686                                   {\r
687                                           'blockquote' : function( element )\r
688                                           {\r
689                                                   var quoted = new CKEDITOR.htmlParser.element( 'div' );\r
690                                                   quoted.children = element.children;\r
691                                                   element.children = [ quoted ];\r
692                                                   var citeText = element.attributes.cite;\r
693                                                   if ( citeText )\r
694                                                   {\r
695                                                           var cite = new CKEDITOR.htmlParser.element( 'cite' );\r
696                                                           cite.add( new CKEDITOR.htmlParser.text( citeText.replace( /^"|"$/g, '' ) ) );\r
697                                                           delete element.attributes.cite;\r
698                                                           element.children.unshift( cite );\r
699                                                   }\r
700                                           },\r
701                                           'span' : function( element )\r
702                                           {\r
703                                                   var bbcode;\r
704                                                   if ( ( bbcode = element.attributes.bbcode ) )\r
705                                                   {\r
706                                                           if ( bbcode == 'img' )\r
707                                                           {\r
708                                                                   element.name = 'img';\r
709                                                                   element.attributes.src = element.children[ 0 ].value;\r
710                                                                   element.children = [];\r
711                                                           }\r
712                                                           else if ( bbcode == 'email' )\r
713                                                           {\r
714                                                                   element.name = 'a';\r
715                                                                   element.attributes.href = 'mailto:' + element.children[ 0 ].value;\r
716                                                           }\r
717 \r
718                                                           delete element.attributes.bbcode;\r
719                                                   }\r
720                                           },\r
721                                           'ol' : function ( element )\r
722                                           {\r
723                                                   if ( element.attributes.listType )\r
724                                                   {\r
725                                                           if ( element.attributes.listType != 'decimal' )\r
726                                                                   element.attributes.style = 'list-style-type:' + element.attributes.listType;\r
727                                                   }\r
728                                                   else\r
729                                                           element.name = 'ul';\r
730 \r
731                                                   delete element.attributes.listType;\r
732                                           },\r
733                                           a : function( element )\r
734                                           {\r
735                                                   if ( !element.attributes.href )\r
736                                                           element.attributes.href = element.children[ 0 ].value;\r
737                                           },\r
738                                           'smiley' : function( element )\r
739                                           {\r
740                                                         element.name = 'img';\r
741 \r
742                                                         var description = element.attributes.desc,\r
743                                                                 image = config.smiley_images[ CKEDITOR.tools.indexOf( config.smiley_descriptions, description ) ],\r
744                                                                 src = CKEDITOR.tools.htmlEncode( config.smiley_path + image );\r
745 \r
746                                                   element.attributes =\r
747                                                   {\r
748                                                           src : src,\r
749                                                           'data-cke-saved-src' : src,\r
750                                                           title :  description,\r
751                                                           alt : description\r
752                                                   };\r
753                                           }\r
754                                   }\r
755                           } );\r
756 \r
757                           editor.dataProcessor.htmlFilter.addRules(\r
758                           {\r
759                                 elements :\r
760                                 {\r
761                                         $ : function( element )\r
762                                         {\r
763                                                 var attributes = element.attributes,\r
764                                                                 style = parseStyleText( attributes.style ),\r
765                                                                 value;\r
766 \r
767                                                 var tagName = element.name;\r
768                                                 if ( tagName in convertMap )\r
769                                                         tagName = convertMap[ tagName ];\r
770                                                 else if ( tagName == 'span' )\r
771                                                 {\r
772                                                         if ( ( value = style.color ) )\r
773                                                         {\r
774                                                                 tagName = 'color';\r
775                                                                 value = RGBToHex( value );\r
776                                                         }\r
777                                                         else if ( ( value = style[ 'font-size' ] ) )\r
778                                                         {\r
779                                                                 var percentValue = value.match( /(\d+)%$/ );\r
780                                                                 if ( percentValue )\r
781                                                                 {\r
782                                                                         value = percentValue[ 1 ];\r
783                                                                         tagName = 'size';\r
784                                                                 }\r
785                                                         }\r
786                                                 }\r
787                                                 else if ( tagName == 'ol' || tagName == 'ul' )\r
788                                                 {\r
789                                                         if ( ( value = style[ 'list-style-type'] ) )\r
790                                                         {\r
791                                                                 switch ( value )\r
792                                                                 {\r
793                                                                         case 'lower-alpha':\r
794                                                                                 value = 'a';\r
795                                                                                 break;\r
796                                                                         case 'upper-alpha':\r
797                                                                                 value = 'A';\r
798                                                                                 break;\r
799                                                                 }\r
800                                                         }\r
801                                                         else if ( tagName == 'ol' )\r
802                                                                 value = 1;\r
803 \r
804                                                         tagName = 'list';\r
805                                                 }\r
806                                                 else if ( tagName == 'blockquote' )\r
807                                                 {\r
808                                                         try\r
809                                                         {\r
810                                                                 var cite = element.children[ 0 ],\r
811                                                                                 quoted = element.children[ 1 ],\r
812                                                                                 citeText = cite.name == 'cite' && cite.children[ 0 ].value;\r
813 \r
814                                                                 if ( citeText )\r
815                                                                 {\r
816                                                                         value = '"' + citeText + '"';\r
817                                                                         element.children = quoted.children;\r
818                                                                 }\r
819 \r
820                                                         }\r
821                                                         catch( er )\r
822                                                         {\r
823                                                         }\r
824 \r
825                                                         tagName = 'quote';\r
826                                                 }\r
827                                                 else if ( tagName == 'a' )\r
828                                                 {\r
829                                                         if ( ( value = attributes.href ) )\r
830                                                         {\r
831                                                                 if ( value.indexOf( 'mailto:' ) !== -1 )\r
832                                                                 {\r
833                                                                         tagName = 'email';\r
834                                                                         // [email] should have a single text child with email address.\r
835                                                                         element.children = [ new CKEDITOR.htmlParser.text( value.replace( 'mailto:', '' ) ) ];\r
836                                                                         value = '';\r
837                                                                 }\r
838                                                                 else\r
839                                                                 {\r
840                                                                         var singleton = element.children.length == 1 && element.children[ 0 ];\r
841                                                                         if ( singleton\r
842                                                                                         && singleton.type == CKEDITOR.NODE_TEXT\r
843                                                                                         && singleton.value == value )\r
844                                                                                 value = '';\r
845 \r
846                                                                         tagName = 'url';\r
847                                                                 }\r
848                                                         }\r
849                                                 }\r
850                                                 else if ( tagName == 'img' )\r
851                                                 {\r
852                                                         element.isEmpty = 0;\r
853 \r
854                                                         // Translate smiley (image) to text emotion.\r
855                                                         var src = attributes[ 'data-cke-saved-src' ];\r
856                                                         if ( src && src.indexOf( editor.config.smiley_path ) != -1 )\r
857                                                                 return new CKEDITOR.htmlParser.text( smileyMap[ attributes.alt ] );\r
858                                                         else\r
859                                                                 element.children = [ new CKEDITOR.htmlParser.text( src ) ];\r
860                                                 }\r
861 \r
862                                                 element.name = tagName;\r
863                                                 value && ( element.attributes.option = value );\r
864 \r
865                                                 return null;\r
866                                         },\r
867 \r
868                                         // Remove any bogus br from the end of a pseudo block,\r
869                                         // e.g. <div>some text<br /><p>paragraph</p></div>\r
870                                         br : function( element )\r
871                                         {\r
872                                                 var next = element.next;\r
873                                                 if ( next && next.name in blockLikeTags )\r
874                                                         return false;\r
875                                         }\r
876                                 }\r
877                           }, 1 );\r
878 \r
879                           editor.dataProcessor.writer = BBCodeWriter;\r
880 \r
881                           editor.on( 'beforeSetMode', function( evt )\r
882                           {\r
883                                   evt.removeListener();\r
884                                   var wysiwyg = editor._.modes[ 'wysiwyg' ];\r
885                                   wysiwyg.loadData = CKEDITOR.tools.override( wysiwyg.loadData, function( org )\r
886                                   {\r
887                                           return function( data )\r
888                                           {\r
889                                                   return ( org.call( this, BBCodeToHtml( data ) ) );\r
890                                           };\r
891                                   } );\r
892                           } );\r
893                   },\r
894 \r
895                   afterInit : function( editor )\r
896                   {\r
897                           var filters;\r
898                           if ( editor._.elementsPath  )\r
899                           {\r
900                                   // Eliminate irrelevant elements from displaying, e.g body and p.\r
901                                   if ( ( filters = editor._.elementsPath.filters ) )\r
902                                         filters.push( function( element )\r
903                                                 {\r
904                                                         var htmlName = element.getName(),\r
905                                                                 name = tagnameMap[ htmlName ] || false;\r
906 \r
907                                                         // Specialized anchor presents as email.\r
908                                                         if ( name == 'link' && element.getAttribute( 'href' ).indexOf( 'mailto:' ) === 0 )\r
909                                                                 name = 'email';\r
910                                                         // Styled span could be either size or color.\r
911                                                         else if ( htmlName == 'span' )\r
912                                                         {\r
913                                                                 if ( element.getStyle( 'font-size' ) )\r
914                                                                         name = 'size';\r
915                                                                 else if ( element.getStyle( 'color' ) )\r
916                                                                         name = 'color';\r
917                                                         }\r
918                                                         else if ( name == 'img' )\r
919                                                         {\r
920                                                                 var src = element.data( 'cke-saved-src' );\r
921                                                                 if ( src && src.indexOf( editor.config.smiley_path ) === 0 )\r
922                                                                         name = 'smiley';\r
923                                                         }\r
924 \r
925                                                         return name;\r
926                                                 });\r
927                           }\r
928                   }\r
929           } );\r
930 \r
931 })();\r