/* Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved. For licensing, see LICENSE.html or http://ckeditor.com/license */ (function() { CKEDITOR.on( 'dialogDefinition', function( ev ) { var tab, name = ev.data.name, definition = ev.data.definition; if ( name == 'link' ) { definition.removeContents( 'target' ); definition.removeContents( 'upload' ); definition.removeContents( 'advanced' ); tab = definition.getContents( 'info' ); tab.remove( 'emailSubject' ); tab.remove( 'emailBody' ); } else if ( name == 'image' ) { definition.removeContents( 'advanced' ); tab = definition.getContents( 'Link' ); tab.remove( 'cmbTarget' ); tab = definition.getContents( 'info' ); tab.remove( 'txtAlt' ); tab.remove( 'basic' ); } }); 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' }, convertMap = { 'strong' : 'b' , 'b' : 'b', 'u': 'u', 'em' : 'i', 'i': 'i', 'code' : 'code', 'li' : '*' }, tagnameMap = { 'strong' : 'b', 'em' : 'i', 'u' : 'u', 'li' : '*', 'ul' : 'list', 'ol' : 'list', 'code' : 'code', 'a' : 'link', 'img' : 'img', 'blockquote' : 'quote' }, stylesMap = { 'color' : 'color', 'size' : 'font-size' }, attributesMap = { 'url' : 'href', 'email' : 'mailhref', 'quote': 'cite', 'list' : 'listType' }; // List of block-like tags. var dtd = CKEDITOR.dtd, blockLikeTags = CKEDITOR.tools.extend( { table:1 }, dtd.$block, dtd.$listItem, dtd.$tableContent, dtd.$list ); var semicolonFixRegex = /\s*(?:;\s*|$)/; function serializeStyleText( stylesObject ) { var styleText = ''; for ( var style in stylesObject ) { var styleVal = stylesObject[ style ], text = ( style + ':' + styleVal ).replace( semicolonFixRegex, ';' ); styleText += text; } return styleText; } function parseStyleText( styleText ) { var retval = {}; ( styleText || '' ) .replace( /"/g, '"' ) .replace( /\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function( match, name, value ) { retval[ name.toLowerCase() ] = value; } ); return retval; } function RGBToHex( cssStyle ) { return cssStyle.replace( /(?:rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\))/gi, function( match, red, green, blue ) { red = parseInt( red, 10 ).toString( 16 ); green = parseInt( green, 10 ).toString( 16 ); blue = parseInt( blue, 10 ).toString( 16 ); var color = [red, green, blue] ; // Add padding zeros if the hex value is less than 0x10. for ( var i = 0 ; i < color.length ; i++ ) color[i] = String( '0' + color[i] ).slice( -2 ) ; return '#' + color.join( '' ) ; }); } // Maintain the map of smiley-to-description. var smileyMap = {"smiley":":)","sad":":(","wink":";)","laugh":":D","cheeky":":P","blush":":*)","surprise":":-o","indecision":":|","angry":">:(","angel":"o:)","cool":"8-)","devil":">:-)","crying":";(","kiss":":-*" }, smileyReverseMap = {}, smileyRegExp = []; // Build regexp for the list of smiley text. for ( var i in smileyMap ) { smileyReverseMap[ smileyMap[ i ] ] = i; smileyRegExp.push( smileyMap[ i ].replace( /\(|\)|\:|\/|\*|\-|\|/g, function( match ) { return '\\' + match; } ) ); } smileyRegExp = new RegExp( smileyRegExp.join( '|' ), 'g' ); var decodeHtml = ( function () { var regex = [], entities = { nbsp : '\u00A0', // IE | FF shy : '\u00AD', // IE gt : '\u003E', // IE | FF | -- | Opera lt : '\u003C' // IE | FF | Safari | Opera }; for ( var entity in entities ) regex.push( entity ); regex = new RegExp( '&(' + regex.join( '|' ) + ');', 'g' ); return function( html ) { return html.replace( regex, function( match, entity ) { return entities[ entity ]; }); }; })(); CKEDITOR.BBCodeParser = function() { this._ = { bbcPartsRegex : /(?:\[([^\/\]=]*?)(?:=([^\]]*?))?\])|(?:\[\/([a-z]{1,16})\])/ig }; }; CKEDITOR.BBCodeParser.prototype = { parse : function( bbcode ) { var parts, part, lastIndex = 0; while ( ( parts = this._.bbcPartsRegex.exec( bbcode ) ) ) { var tagIndex = parts.index; if ( tagIndex > lastIndex ) { var text = bbcode.substring( lastIndex, tagIndex ); this.onText( text, 1 ); } lastIndex = this._.bbcPartsRegex.lastIndex; /* "parts" is an array with the following items: 0 : The entire match for opening/closing tags and line-break; 1 : line-break; 2 : open of tag excludes option; 3 : tag option; 4 : close of tag; */ part = ( parts[ 1 ] || parts[ 3 ] || '' ).toLowerCase(); // Unrecognized tags should be delivered as a simple text (#7860). if ( part && !bbcodeMap[ part ] ) { this.onText( parts[ 0 ] ); continue; } // Opening tag if ( parts[ 1 ] ) { var tagName = bbcodeMap[ part ], attribs = {}, styles = {}, optionPart = parts[ 2 ]; if ( optionPart ) { if ( part == 'list' ) { if ( !isNaN( optionPart ) ) optionPart = 'decimal'; else if ( /^[a-z]+$/.test( optionPart ) ) optionPart = 'lower-alpha'; else if ( /^[A-Z]+$/.test( optionPart ) ) optionPart = 'upper-alpha'; } if ( stylesMap[ part ] ) { // Font size represents percentage. if ( part == 'size' ) optionPart += '%'; styles[ stylesMap[ part ] ] = optionPart; attribs.style = serializeStyleText( styles ); } else if ( attributesMap[ part ] ) attribs[ attributesMap[ part ] ] = optionPart; } // Two special handling - image and email, protect them // as "span" with an attribute marker. if ( part == 'email' || part == 'img' ) attribs[ 'bbcode' ] = part; this.onTagOpen( tagName, attribs, CKEDITOR.dtd.$empty[ tagName ] ); } // Closing tag else if ( parts[ 3 ] ) this.onTagClose( bbcodeMap[ part ] ); } if ( bbcode.length > lastIndex ) this.onText( bbcode.substring( lastIndex, bbcode.length ), 1 ); } }; /** * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string. * @param {String} source The HTML to be parsed, filling the fragment. * @param {Number} [fixForBody=false] Wrap body with specified element if needed. * @returns CKEDITOR.htmlParser.fragment The fragment created. * @example * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( 'Sample Text' ); * alert( fragment.children[0].name ); "b" * alert( fragment.children[1].value ); " Text" */ CKEDITOR.htmlParser.fragment.fromBBCode = function( source ) { var parser = new CKEDITOR.BBCodeParser(), fragment = new CKEDITOR.htmlParser.fragment(), pendingInline = [], pendingBrs = 0, currentNode = fragment, returnPoint; function checkPending( newTagName ) { if ( pendingInline.length > 0 ) { for ( var i = 0 ; i < pendingInline.length ; i++ ) { var pendingElement = pendingInline[ i ], pendingName = pendingElement.name, pendingDtd = CKEDITOR.dtd[ pendingName ], currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ]; if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) ) { // Get a clone for the pending element. pendingElement = pendingElement.clone(); // Add it to the current node and make it the current, // so the new element will be added inside of it. pendingElement.parent = currentNode; currentNode = pendingElement; // Remove the pending element (back the index by one // to properly process the next entry). pendingInline.splice( i, 1 ); i--; } } } } function checkPendingBrs( tagName, closing ) { var len = currentNode.children.length, previous = len > 0 && currentNode.children[ len - 1 ], lineBreakParent = !previous && BBCodeWriter.getRule( tagnameMap[ currentNode.name ], 'breakAfterOpen' ), lineBreakPrevious = previous && previous.type == CKEDITOR.NODE_ELEMENT && BBCodeWriter.getRule( tagnameMap[ previous.name ], 'breakAfterClose' ), lineBreakCurrent = tagName && BBCodeWriter.getRule( tagnameMap[ tagName ], closing ? 'breakBeforeClose' : 'breakBeforeOpen' ); if ( pendingBrs && ( lineBreakParent || lineBreakPrevious || lineBreakCurrent ) ) pendingBrs--; // 1. Either we're at the end of block, where it requires us to compensate the br filler // removing logic (from htmldataprocessor). // 2. Or we're at the end of pseudo block, where it requires us to compensate // the bogus br effect. if ( pendingBrs && tagName in blockLikeTags ) pendingBrs++; while ( pendingBrs && pendingBrs-- ) currentNode.children.push( previous = new CKEDITOR.htmlParser.element( 'br' ) ); } function addElement( node, target ) { checkPendingBrs( node.name, 1 ); target = target || currentNode || fragment; var len = target.children.length, previous = len > 0 && target.children[ len - 1 ] || null; node.previous = previous; node.parent = target; target.children.push( node ); if ( node.returnPoint ) { currentNode = node.returnPoint; delete node.returnPoint; } } parser.onTagOpen = function( tagName, attributes, selfClosing ) { var element = new CKEDITOR.htmlParser.element( tagName, attributes ); // This is a tag to be removed if empty, so do not add it immediately. if ( CKEDITOR.dtd.$removeEmpty[ tagName ] ) { pendingInline.push( element ); return; } var currentName = currentNode.name; var currentDtd = currentName && ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ); // If the element cannot be child of the current element. if ( currentDtd && !currentDtd[ tagName ] ) { var reApply = false, addPoint; // New position to start adding nodes. // If the element name is the same as the current element name, // then just close the current one and append the new one to the // parent. This situation usually happens with

,

  • ,
    and //
    , specially in IE. Do not enter in this if block in this case. if ( tagName == currentName ) addElement( currentNode, currentNode.parent ); else if ( tagName in CKEDITOR.dtd.$listItem ) { parser.onTagOpen( 'ul', {} ); addPoint = currentNode; reApply = true; } else { addElement( currentNode, currentNode.parent ); // The current element is an inline element, which // cannot hold the new one. Put it in the pending list, // and try adding the new one after it. pendingInline.unshift( currentNode ); reApply = true; } if ( addPoint ) currentNode = addPoint; // Try adding it to the return point, or the parent element. else currentNode = currentNode.returnPoint || currentNode.parent; if ( reApply ) { parser.onTagOpen.apply( this, arguments ); return; } } checkPending( tagName ); checkPendingBrs( tagName ); element.parent = currentNode; element.returnPoint = returnPoint; returnPoint = 0; if ( element.isEmpty ) addElement( element ); else currentNode = element; }; parser.onTagClose = function( tagName ) { // Check if there is any pending tag to be closed. for ( var i = pendingInline.length - 1 ; i >= 0 ; i-- ) { // If found, just remove it from the list. if ( tagName == pendingInline[ i ].name ) { pendingInline.splice( i, 1 ); return; } } var pendingAdd = [], newPendingInline = [], candidate = currentNode; while ( candidate.type && candidate.name != tagName ) { // If this is an inline element, add it to the pending list, if we're // really closing one of the parents element later, they will continue // after it. if ( !candidate._.isBlockLike ) newPendingInline.unshift( candidate ); // This node should be added to it's parent at this point. But, // it should happen only if the closing tag is really closing // one of the nodes. So, for now, we just cache it. pendingAdd.push( candidate ); candidate = candidate.parent; } if ( candidate.type ) { // Add all elements that have been found in the above loop. for ( i = 0 ; i < pendingAdd.length ; i++ ) { var node = pendingAdd[ i ]; addElement( node, node.parent ); } currentNode = candidate; addElement( candidate, candidate.parent ); // The parent should start receiving new nodes now, except if // addElement changed the currentNode. if ( candidate == currentNode ) currentNode = currentNode.parent; pendingInline = pendingInline.concat( newPendingInline ); } }; parser.onText = function( text ) { var currentDtd = CKEDITOR.dtd[ currentNode.name ]; if ( !currentDtd || currentDtd[ '#' ] ) { checkPendingBrs(); checkPending(); text.replace(/([\r\n])|[^\r\n]*/g, function( piece, lineBreak ) { if ( lineBreak !== undefined && lineBreak.length ) pendingBrs++; else if ( piece.length ) { var lastIndex = 0; // Create smiley from text emotion. piece.replace( smileyRegExp, function( match, index ) { addElement( new CKEDITOR.htmlParser.text( piece.substring( lastIndex, index ) ), currentNode ); addElement( new CKEDITOR.htmlParser.element( 'smiley', { 'desc': smileyReverseMap[ match ] } ), currentNode ); lastIndex = index + match.length; }); if ( lastIndex != piece.length ) addElement( new CKEDITOR.htmlParser.text( piece.substring( lastIndex, piece.length ) ), currentNode ); } }); } }; // Parse it. parser.parse( CKEDITOR.tools.htmlEncode( source ) ); // Close all hanging nodes. while ( currentNode.type ) { var parent = currentNode.parent, node = currentNode; addElement( node, parent ); currentNode = parent; } return fragment; }; CKEDITOR.htmlParser.BBCodeWriter = CKEDITOR.tools.createClass( { $ : function() { this._ = { output : [], rules : [] }; // List and list item. this.setRules( 'list', { breakBeforeOpen : 1, breakAfterOpen : 1, breakBeforeClose : 1, breakAfterClose : 1 } ); this.setRules( '*', { breakBeforeOpen : 1, breakAfterOpen : 0, breakBeforeClose : 1, breakAfterClose : 0 } ); this.setRules( 'quote', { breakBeforeOpen : 1, breakAfterOpen : 0, breakBeforeClose : 0, breakAfterClose : 1 } ); }, proto : { /** * Sets formatting rules for a given tag. The possible rules are: * * * All rules default to "false". Each call to the function overrides * already present rules, leaving the undefined untouched. * * @param {String} tagName The tag name to which set the rules. * @param {Object} rules An object containing the element rules. * @example * // Break line before and after "img" tags. * writer.setRules( 'list', * { * breakBeforeOpen : true * breakAfterOpen : true * }); */ setRules : function( tagName, rules ) { var currentRules = this._.rules[ tagName ]; if ( currentRules ) CKEDITOR.tools.extend( currentRules, rules, true ); else this._.rules[ tagName ] = rules; }, getRule : function( tagName, ruleName ) { return this._.rules[ tagName ] && this._.rules[ tagName ][ ruleName ]; }, openTag : function( tag, attributes ) { if ( tag in bbcodeMap ) { if ( this.getRule( tag, 'breakBeforeOpen' ) ) this.lineBreak( 1 ); this.write( '[', tag ); var option = attributes.option; option && this.write( '=', option ); this.write( ']' ); if ( this.getRule( tag, 'breakAfterOpen' ) ) this.lineBreak( 1 ); } else if ( tag == 'br' ) this._.output.push( '\n' ); }, openTagClose : function() { }, attribute : function() { }, closeTag : function( tag ) { if ( tag in bbcodeMap ) { if ( this.getRule( tag, 'breakBeforeClose' ) ) this.lineBreak( 1 ); tag != '*' && this.write( '[/', tag, ']' ); if ( this.getRule( tag, 'breakAfterClose' ) ) this.lineBreak( 1 ); } }, text : function( text ) { this.write( text ); }, /** * Writes a comment. * @param {String} comment The comment text. * @example * // Writes "<!-- My comment -->". * writer.comment( ' My comment ' ); */ comment : function() {}, /* * Output line-break for formatting. */ lineBreak : function() { // Avoid line break when: // 1) Previous tag already put one. // 2) We're at output start. if ( !this._.hasLineBreak && this._.output.length ) { this.write( '\n' ); this._.hasLineBreak = 1; } }, write : function() { this._.hasLineBreak = 0; var data = Array.prototype.join.call( arguments, '' ); this._.output.push( data ); }, reset : function() { this._.output = []; this._.hasLineBreak = 0; }, getHtml : function( reset ) { var bbcode = this._.output.join( '' ); if ( reset ) this.reset(); return decodeHtml ( bbcode ); } } }); var BBCodeWriter = new CKEDITOR.htmlParser.BBCodeWriter(); CKEDITOR.plugins.add( 'bbcode', { requires : [ 'htmldataprocessor', 'entities' ], beforeInit : function( editor ) { // Adapt some critical editor configuration for better support // of BBCode environment. var config = editor.config; CKEDITOR.tools.extend( config, { enterMode : CKEDITOR.ENTER_BR, basicEntities: false, entities : false, fillEmptyBlocks : false }, true ); }, init : function( editor ) { var config = editor.config; function BBCodeToHtml( code ) { var fragment = CKEDITOR.htmlParser.fragment.fromBBCode( code ), writer = new CKEDITOR.htmlParser.basicWriter(); fragment.writeHtml( writer, dataFilter ); return writer.getHtml( true ); } var dataFilter = new CKEDITOR.htmlParser.filter(); dataFilter.addRules( { elements : { 'blockquote' : function( element ) { var quoted = new CKEDITOR.htmlParser.element( 'div' ); quoted.children = element.children; element.children = [ quoted ]; var citeText = element.attributes.cite; if ( citeText ) { var cite = new CKEDITOR.htmlParser.element( 'cite' ); cite.add( new CKEDITOR.htmlParser.text( citeText.replace( /^"|"$/g, '' ) ) ); delete element.attributes.cite; element.children.unshift( cite ); } }, 'span' : function( element ) { var bbcode; if ( ( bbcode = element.attributes.bbcode ) ) { if ( bbcode == 'img' ) { element.name = 'img'; element.attributes.src = element.children[ 0 ].value; element.children = []; } else if ( bbcode == 'email' ) { element.name = 'a'; element.attributes.href = 'mailto:' + element.children[ 0 ].value; } delete element.attributes.bbcode; } }, 'ol' : function ( element ) { if ( element.attributes.listType ) { if ( element.attributes.listType != 'decimal' ) element.attributes.style = 'list-style-type:' + element.attributes.listType; } else element.name = 'ul'; delete element.attributes.listType; }, a : function( element ) { if ( !element.attributes.href ) element.attributes.href = element.children[ 0 ].value; }, 'smiley' : function( element ) { element.name = 'img'; var description = element.attributes.desc, image = config.smiley_images[ CKEDITOR.tools.indexOf( config.smiley_descriptions, description ) ], src = CKEDITOR.tools.htmlEncode( config.smiley_path + image ); element.attributes = { src : src, 'data-cke-saved-src' : src, title : description, alt : description }; } } } ); editor.dataProcessor.htmlFilter.addRules( { elements : { $ : function( element ) { var attributes = element.attributes, style = parseStyleText( attributes.style ), value; var tagName = element.name; if ( tagName in convertMap ) tagName = convertMap[ tagName ]; else if ( tagName == 'span' ) { if ( ( value = style.color ) ) { tagName = 'color'; value = RGBToHex( value ); } else if ( ( value = style[ 'font-size' ] ) ) { var percentValue = value.match( /(\d+)%$/ ); if ( percentValue ) { value = percentValue[ 1 ]; tagName = 'size'; } } } else if ( tagName == 'ol' || tagName == 'ul' ) { if ( ( value = style[ 'list-style-type'] ) ) { switch ( value ) { case 'lower-alpha': value = 'a'; break; case 'upper-alpha': value = 'A'; break; } } else if ( tagName == 'ol' ) value = 1; tagName = 'list'; } else if ( tagName == 'blockquote' ) { try { var cite = element.children[ 0 ], quoted = element.children[ 1 ], citeText = cite.name == 'cite' && cite.children[ 0 ].value; if ( citeText ) { value = '"' + citeText + '"'; element.children = quoted.children; } } catch( er ) { } tagName = 'quote'; } else if ( tagName == 'a' ) { if ( ( value = attributes.href ) ) { if ( value.indexOf( 'mailto:' ) !== -1 ) { tagName = 'email'; // [email] should have a single text child with email address. element.children = [ new CKEDITOR.htmlParser.text( value.replace( 'mailto:', '' ) ) ]; value = ''; } else { var singleton = element.children.length == 1 && element.children[ 0 ]; if ( singleton && singleton.type == CKEDITOR.NODE_TEXT && singleton.value == value ) value = ''; tagName = 'url'; } } } else if ( tagName == 'img' ) { element.isEmpty = 0; // Translate smiley (image) to text emotion. var src = attributes[ 'data-cke-saved-src' ]; if ( src && src.indexOf( editor.config.smiley_path ) != -1 ) return new CKEDITOR.htmlParser.text( smileyMap[ attributes.alt ] ); else element.children = [ new CKEDITOR.htmlParser.text( src ) ]; } element.name = tagName; value && ( element.attributes.option = value ); return null; }, // Remove any bogus br from the end of a pseudo block, // e.g.
    some text

    paragraph

    br : function( element ) { var next = element.next; if ( next && next.name in blockLikeTags ) return false; } } }, 1 ); editor.dataProcessor.writer = BBCodeWriter; editor.on( 'beforeSetMode', function( evt ) { evt.removeListener(); var wysiwyg = editor._.modes[ 'wysiwyg' ]; wysiwyg.loadData = CKEDITOR.tools.override( wysiwyg.loadData, function( org ) { return function( data ) { return ( org.call( this, BBCodeToHtml( data ) ) ); }; } ); } ); }, afterInit : function( editor ) { var filters; if ( editor._.elementsPath ) { // Eliminate irrelevant elements from displaying, e.g body and p. if ( ( filters = editor._.elementsPath.filters ) ) filters.push( function( element ) { var htmlName = element.getName(), name = tagnameMap[ htmlName ] || false; // Specialized anchor presents as email. if ( name == 'link' && element.getAttribute( 'href' ).indexOf( 'mailto:' ) === 0 ) name = 'email'; // Styled span could be either size or color. else if ( htmlName == 'span' ) { if ( element.getStyle( 'font-size' ) ) name = 'size'; else if ( element.getStyle( 'color' ) ) name = 'color'; } else if ( name == 'img' ) { var src = element.data( 'cke-saved-src' ); if ( src && src.indexOf( editor.config.smiley_path ) === 0 ) name = 'smiley'; } return name; }); } } } ); })();