1 /* 2 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved. 3 For licensing, see LICENSE.html or http://ckeditor.com/license 4 */ 5 6 (function() 7 { 8 // Regex to scan for at the end of blocks, which are actually placeholders. 9 // Safari transforms the to \xa0. (#4172) 10 var tailNbspRegex = /^[\t\r\n ]*(?: |\xa0)$/; 11 12 var protectedSourceMarker = '{cke_protected}'; 13 14 // Return the last non-space child node of the block (#4344). 15 function lastNoneSpaceChild( block ) 16 { 17 var lastIndex = block.children.length, 18 last = block.children[ lastIndex - 1 ]; 19 while ( last && last.type == CKEDITOR.NODE_TEXT && !CKEDITOR.tools.trim( last.value ) ) 20 last = block.children[ --lastIndex ]; 21 return last; 22 } 23 24 function trimFillers( block, fromSource ) 25 { 26 // If the current node is a block, and if we're converting from source or 27 // we're not in IE then search for and remove any tailing BR node. 28 // 29 // Also, any at the end of blocks are fillers, remove them as well. 30 // (#2886) 31 var children = block.children, lastChild = lastNoneSpaceChild( block ); 32 if ( lastChild ) 33 { 34 if ( ( fromSource || !CKEDITOR.env.ie ) && lastChild.type == CKEDITOR.NODE_ELEMENT && lastChild.name == 'br' ) 35 children.pop(); 36 if ( lastChild.type == CKEDITOR.NODE_TEXT && tailNbspRegex.test( lastChild.value ) ) 37 children.pop(); 38 } 39 } 40 41 function blockNeedsExtension( block, fromSource, extendEmptyBlock ) 42 { 43 if( !fromSource && ( !extendEmptyBlock || 44 typeof extendEmptyBlock == 'function' && ( extendEmptyBlock( block ) === false ) ) ) 45 return false; 46 47 // 1. For IE version >=8, empty blocks are displayed correctly themself in wysiwiyg; 48 // 2. For the rest, at least table cell and list item need no filler space. 49 // (#6248) 50 if ( fromSource && CKEDITOR.env.ie && 51 ( document.documentMode > 7 52 || block.name in CKEDITOR.dtd.tr 53 || block.name in CKEDITOR.dtd.$listItem ) ) 54 return false; 55 56 var lastChild = lastNoneSpaceChild( block ); 57 58 return !lastChild || lastChild && 59 ( lastChild.type == CKEDITOR.NODE_ELEMENT && lastChild.name == 'br' 60 // Some of the controls in form needs extension too, 61 // to move cursor at the end of the form. (#4791) 62 || block.name == 'form' && lastChild.name == 'input' ); 63 } 64 65 function getBlockExtension( isOutput, emptyBlockFiller ) 66 { 67 return function( node ) 68 { 69 trimFillers( node, !isOutput ); 70 71 if ( blockNeedsExtension( node, !isOutput, emptyBlockFiller ) ) 72 { 73 if ( isOutput || CKEDITOR.env.ie ) 74 node.add( new CKEDITOR.htmlParser.text( '\xa0' ) ); 75 else 76 node.add( new CKEDITOR.htmlParser.element( 'br', {} ) ); 77 } 78 }; 79 } 80 81 var dtd = CKEDITOR.dtd; 82 83 // Define orders of table elements. 84 var tableOrder = [ 'caption', 'colgroup', 'col', 'thead', 'tfoot', 'tbody' ]; 85 86 // Find out the list of block-like tags that can contain <br>. 87 var blockLikeTags = CKEDITOR.tools.extend( {}, dtd.$block, dtd.$listItem, dtd.$tableContent ); 88 for ( var i in blockLikeTags ) 89 { 90 if ( ! ( 'br' in dtd[i] ) ) 91 delete blockLikeTags[i]; 92 } 93 // We just avoid filler in <pre> right now. 94 // TODO: Support filler for <pre>, line break is also occupy line height. 95 delete blockLikeTags.pre; 96 var defaultDataFilterRules = 97 { 98 elements : {}, 99 attributeNames : 100 [ 101 // Event attributes (onXYZ) must not be directly set. They can become 102 // active in the editing area (IE|WebKit). 103 [ ( /^on/ ), 'data-cke-pa-on' ] 104 ] 105 }; 106 107 var defaultDataBlockFilterRules = { elements : {} }; 108 109 for ( i in blockLikeTags ) 110 defaultDataBlockFilterRules.elements[ i ] = getBlockExtension(); 111 112 var defaultHtmlFilterRules = 113 { 114 elementNames : 115 [ 116 // Remove the "cke:" namespace prefix. 117 [ ( /^cke:/ ), '' ], 118 119 // Ignore <?xml:namespace> tags. 120 [ ( /^\?xml:namespace$/ ), '' ] 121 ], 122 123 attributeNames : 124 [ 125 // Attributes saved for changes and protected attributes. 126 [ ( /^data-cke-(saved|pa)-/ ), '' ], 127 128 // All "data-cke-" attributes are to be ignored. 129 [ ( /^data-cke-.*/ ), '' ], 130 131 [ 'hidefocus', '' ] 132 ], 133 134 elements : 135 { 136 $ : function( element ) 137 { 138 var attribs = element.attributes; 139 140 if ( attribs ) 141 { 142 // Elements marked as temporary are to be ignored. 143 if ( attribs[ 'data-cke-temp' ] ) 144 return false; 145 146 // Remove duplicated attributes - #3789. 147 var attributeNames = [ 'name', 'href', 'src' ], 148 savedAttributeName; 149 for ( var i = 0 ; i < attributeNames.length ; i++ ) 150 { 151 savedAttributeName = 'data-cke-saved-' + attributeNames[ i ]; 152 savedAttributeName in attribs && ( delete attribs[ attributeNames[ i ] ] ); 153 } 154 } 155 156 return element; 157 }, 158 159 // The contents of table should be in correct order (#4809). 160 table : function( element ) 161 { 162 var children = element.children; 163 children.sort( function ( node1, node2 ) 164 { 165 return node1.type == CKEDITOR.NODE_ELEMENT && node2.type == node1.type ? 166 CKEDITOR.tools.indexOf( tableOrder, node1.name ) > CKEDITOR.tools.indexOf( tableOrder, node2.name ) ? 1 : -1 : 0; 167 } ); 168 }, 169 170 embed : function( element ) 171 { 172 var parent = element.parent; 173 174 // If the <embed> is child of a <object>, copy the width 175 // and height attributes from it. 176 if ( parent && parent.name == 'object' ) 177 { 178 var parentWidth = parent.attributes.width, 179 parentHeight = parent.attributes.height; 180 parentWidth && ( element.attributes.width = parentWidth ); 181 parentHeight && ( element.attributes.height = parentHeight ); 182 } 183 }, 184 // Restore param elements into self-closing. 185 param : function( param ) 186 { 187 param.children = []; 188 param.isEmpty = true; 189 return param; 190 }, 191 192 // Remove empty link but not empty anchor.(#3829) 193 a : function( element ) 194 { 195 if ( !( element.children.length || 196 element.attributes.name || 197 element.attributes[ 'data-cke-saved-name' ] ) ) 198 { 199 return false; 200 } 201 }, 202 203 // Remove dummy span in webkit. 204 span: function( element ) 205 { 206 if ( element.attributes[ 'class' ] == 'Apple-style-span' ) 207 delete element.name; 208 }, 209 210 // Empty <pre> in IE is reported with filler node ( ). 211 pre : function( element ) { CKEDITOR.env.ie && trimFillers( element ); }, 212 213 html : function( element ) 214 { 215 delete element.attributes.contenteditable; 216 delete element.attributes[ 'class' ]; 217 }, 218 219 body : function( element ) 220 { 221 delete element.attributes.spellcheck; 222 delete element.attributes.contenteditable; 223 }, 224 225 style : function( element ) 226 { 227 var child = element.children[ 0 ]; 228 child && child.value && ( child.value = CKEDITOR.tools.trim( child.value )); 229 230 if ( !element.attributes.type ) 231 element.attributes.type = 'text/css'; 232 }, 233 234 title : function( element ) 235 { 236 var titleText = element.children[ 0 ]; 237 titleText && ( titleText.value = element.attributes[ 'data-cke-title' ] || '' ); 238 } 239 }, 240 241 attributes : 242 { 243 'class' : function( value, element ) 244 { 245 // Remove all class names starting with "cke_". 246 return CKEDITOR.tools.ltrim( value.replace( /(?:^|\s+)cke_[^\s]*/g, '' ) ) || false; 247 } 248 } 249 }; 250 251 if ( CKEDITOR.env.ie ) 252 { 253 // IE outputs style attribute in capital letters. We should convert 254 // them back to lower case, while not hurting the values (#5930) 255 defaultHtmlFilterRules.attributes.style = function( value, element ) 256 { 257 return value.replace( /(^|;)([^\:]+)/g, function( match ) 258 { 259 return match.toLowerCase(); 260 }); 261 }; 262 } 263 264 function protectReadOnly( element ) 265 { 266 var attrs = element.attributes; 267 268 // We should flag that the element was locked by our code so 269 // it'll be editable by the editor functions (#6046). 270 if ( attrs.contenteditable != "false" ) 271 attrs[ 'data-cke-editable' ] = attrs.contenteditable ? 'true' : 1; 272 273 attrs.contenteditable = "false"; 274 } 275 function unprotectReadyOnly( element ) 276 { 277 var attrs = element.attributes; 278 switch( attrs[ 'data-cke-editable' ] ) 279 { 280 case 'true': attrs.contenteditable = 'true'; break; 281 case '1': delete attrs.contenteditable; break; 282 } 283 } 284 // Disable form elements editing mode provided by some browers. (#5746) 285 for ( i in { input : 1, textarea : 1 } ) 286 { 287 defaultDataFilterRules.elements[ i ] = protectReadOnly; 288 defaultHtmlFilterRules.elements[ i ] = unprotectReadyOnly; 289 } 290 291 var protectElementRegex = /<(a|area|img|input)\b([^>]*)>/gi, 292 protectAttributeRegex = /\b(on\w+|href|src|name)\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|(?:[^ "'>]+))/gi; 293 294 var protectElementsRegex = /(?:<style(?=[ >])[^>]*>[\s\S]*<\/style>)|(?:<(:?link|meta|base)[^>]*>)/gi, 295 encodedElementsRegex = /<cke:encoded>([^<]*)<\/cke:encoded>/gi; 296 297 var protectElementNamesRegex = /(<\/?)((?:object|embed|param|html|body|head|title)[^>]*>)/gi, 298 unprotectElementNamesRegex = /(<\/?)cke:((?:html|body|head|title)[^>]*>)/gi; 299 300 var protectSelfClosingRegex = /<cke:(param|embed)([^>]*?)\/?>(?!\s*<\/cke:\1)/gi; 301 302 function protectAttributes( html ) 303 { 304 return html.replace( protectElementRegex, function( element, tag, attributes ) 305 { 306 return '<' + tag + attributes.replace( protectAttributeRegex, function( fullAttr, attrName ) 307 { 308 // Avoid corrupting the inline event attributes (#7243). 309 // We should not rewrite the existed protected attributes, e.g. clipboard content from editor. (#5218) 310 if ( !( /^on/ ).test( attrName ) && attributes.indexOf( 'data-cke-saved-' + attrName ) == -1 ) 311 return ' data-cke-saved-' + fullAttr + ' data-cke-' + CKEDITOR.rnd + '-' + fullAttr; 312 313 return fullAttr; 314 }) + '>'; 315 }); 316 } 317 318 function protectElements( html ) 319 { 320 return html.replace( protectElementsRegex, function( match ) 321 { 322 return '<cke:encoded>' + encodeURIComponent( match ) + '</cke:encoded>'; 323 }); 324 } 325 326 function unprotectElements( html ) 327 { 328 return html.replace( encodedElementsRegex, function( match, encoded ) 329 { 330 return decodeURIComponent( encoded ); 331 }); 332 } 333 334 function protectElementsNames( html ) 335 { 336 return html.replace( protectElementNamesRegex, '$1cke:$2'); 337 } 338 339 function unprotectElementNames( html ) 340 { 341 return html.replace( unprotectElementNamesRegex, '$1$2' ); 342 } 343 344 function protectSelfClosingElements( html ) 345 { 346 return html.replace( protectSelfClosingRegex, '<cke:$1$2></cke:$1>' ); 347 } 348 349 function protectPreFormatted( html ) 350 { 351 return html.replace( /(<pre\b[^>]*>)(\r\n|\n)/g, '$1$2$2' ); 352 } 353 354 function protectRealComments( html ) 355 { 356 return html.replace( /<!--(?!{cke_protected})[\s\S]+?-->/g, function( match ) 357 { 358 return '<!--' + protectedSourceMarker + 359 '{C}' + 360 encodeURIComponent( match ).replace( /--/g, '%2D%2D' ) + 361 '-->'; 362 }); 363 } 364 365 function unprotectRealComments( html ) 366 { 367 return html.replace( /<!--\{cke_protected\}\{C\}([\s\S]+?)-->/g, function( match, data ) 368 { 369 return decodeURIComponent( data ); 370 }); 371 } 372 373 function unprotectSource( html, editor ) 374 { 375 var store = editor._.dataStore; 376 377 return html.replace( /<!--\{cke_protected\}([\s\S]+?)-->/g, function( match, data ) 378 { 379 return decodeURIComponent( data ); 380 }).replace( /\{cke_protected_(\d+)\}/g, function( match, id ) 381 { 382 return store && store[ id ] || ''; 383 }); 384 } 385 386 function protectSource( data, editor ) 387 { 388 var protectedHtml = [], 389 protectRegexes = editor.config.protectedSource, 390 store = editor._.dataStore || ( editor._.dataStore = { id : 1 } ), 391 tempRegex = /<\!--\{cke_temp(comment)?\}(\d*?)-->/g; 392 393 var regexes = 394 [ 395 // Script tags will also be forced to be protected, otherwise 396 // IE will execute them. 397 ( /<script[\s\S]*?<\/script>/gi ), 398 399 // <noscript> tags (get lost in IE and messed up in FF). 400 /<noscript[\s\S]*?<\/noscript>/gi 401 ] 402 .concat( protectRegexes ); 403 404 // First of any other protection, we must protect all comments 405 // to avoid loosing them (of course, IE related). 406 // Note that we use a different tag for comments, as we need to 407 // transform them when applying filters. 408 data = data.replace( (/<!--[\s\S]*?-->/g), function( match ) 409 { 410 return '<!--{cke_tempcomment}' + ( protectedHtml.push( match ) - 1 ) + '-->'; 411 }); 412 413 for ( var i = 0 ; i < regexes.length ; i++ ) 414 { 415 data = data.replace( regexes[i], function( match ) 416 { 417 match = match.replace( tempRegex, // There could be protected source inside another one. (#3869). 418 function( $, isComment, id ) 419 { 420 return protectedHtml[ id ]; 421 } 422 ); 423 424 // Avoid protecting over protected, e.g. /\{.*?\}/ 425 return ( /cke_temp(comment)?/ ).test( match ) ? match 426 : '<!--{cke_temp}' + ( protectedHtml.push( match ) - 1 ) + '-->'; 427 }); 428 } 429 data = data.replace( tempRegex, function( $, isComment, id ) 430 { 431 return '<!--' + protectedSourceMarker + 432 ( isComment ? '{C}' : '' ) + 433 encodeURIComponent( protectedHtml[ id ] ).replace( /--/g, '%2D%2D' ) + 434 '-->'; 435 } 436 ); 437 438 // Different protection pattern is used for those that 439 // live in attributes to avoid from being HTML encoded. 440 return data.replace( /(['"]).*?\1/g, function ( match ) 441 { 442 return match.replace( /<!--\{cke_protected\}([\s\S]+?)-->/g, function( match, data ) 443 { 444 store[ store.id ] = decodeURIComponent( data ); 445 return '{cke_protected_'+ ( store.id++ ) + '}'; 446 }); 447 }); 448 } 449 450 CKEDITOR.plugins.add( 'htmldataprocessor', 451 { 452 requires : [ 'htmlwriter' ], 453 454 init : function( editor ) 455 { 456 var dataProcessor = editor.dataProcessor = new CKEDITOR.htmlDataProcessor( editor ); 457 458 dataProcessor.writer.forceSimpleAmpersand = editor.config.forceSimpleAmpersand; 459 460 dataProcessor.dataFilter.addRules( defaultDataFilterRules ); 461 dataProcessor.dataFilter.addRules( defaultDataBlockFilterRules ); 462 dataProcessor.htmlFilter.addRules( defaultHtmlFilterRules ); 463 464 var defaultHtmlBlockFilterRules = { elements : {} }; 465 for ( i in blockLikeTags ) 466 defaultHtmlBlockFilterRules.elements[ i ] = getBlockExtension( true, editor.config.fillEmptyBlocks ); 467 468 dataProcessor.htmlFilter.addRules( defaultHtmlBlockFilterRules ); 469 }, 470 471 onLoad : function() 472 { 473 ! ( 'fillEmptyBlocks' in CKEDITOR.config ) && ( CKEDITOR.config.fillEmptyBlocks = 1 ); 474 } 475 }); 476 477 CKEDITOR.htmlDataProcessor = function( editor ) 478 { 479 this.editor = editor; 480 481 this.writer = new CKEDITOR.htmlWriter(); 482 this.dataFilter = new CKEDITOR.htmlParser.filter(); 483 this.htmlFilter = new CKEDITOR.htmlParser.filter(); 484 }; 485 486 CKEDITOR.htmlDataProcessor.prototype = 487 { 488 toHtml : function( data, fixForBody ) 489 { 490 // The source data is already HTML, but we need to clean 491 // it up and apply the filter. 492 493 data = protectSource( data, this.editor ); 494 495 // Before anything, we must protect the URL attributes as the 496 // browser may changing them when setting the innerHTML later in 497 // the code. 498 data = protectAttributes( data ); 499 500 // Protect elements than can't be set inside a DIV. E.g. IE removes 501 // style tags from innerHTML. (#3710) 502 data = protectElements( data ); 503 504 // Certain elements has problem to go through DOM operation, protect 505 // them by prefixing 'cke' namespace. (#3591) 506 data = protectElementsNames( data ); 507 508 // All none-IE browsers ignore self-closed custom elements, 509 // protecting them into open-close. (#3591) 510 data = protectSelfClosingElements( data ); 511 512 // Compensate one leading line break after <pre> open as browsers 513 // eat it up. (#5789) 514 data = protectPreFormatted( data ); 515 516 // Call the browser to help us fixing a possibly invalid HTML 517 // structure. 518 var div = new CKEDITOR.dom.element( 'div' ); 519 520 // Add fake character to workaround IE comments bug. (#3801) 521 div.setHtml( 'a' + data ); 522 data = div.getHtml().substr( 1 ); 523 524 // Restore shortly protected attribute names. 525 data = data.replace( new RegExp( ' data-cke-' + CKEDITOR.rnd + '-', 'ig' ), ' ' ); 526 527 // Unprotect "some" of the protected elements at this point. 528 data = unprotectElementNames( data ); 529 530 data = unprotectElements( data ); 531 532 // Restore the comments that have been protected, in this way they 533 // can be properly filtered. 534 data = unprotectRealComments( data ); 535 536 // Now use our parser to make further fixes to the structure, as 537 // well as apply the filter. 538 var fragment = CKEDITOR.htmlParser.fragment.fromHtml( data, fixForBody ), 539 writer = new CKEDITOR.htmlParser.basicWriter(); 540 541 fragment.writeHtml( writer, this.dataFilter ); 542 data = writer.getHtml( true ); 543 544 // Protect the real comments again. 545 data = protectRealComments( data ); 546 547 return data; 548 }, 549 550 toDataFormat : function( html, fixForBody ) 551 { 552 var writer = this.writer, 553 fragment = CKEDITOR.htmlParser.fragment.fromHtml( html, fixForBody ); 554 555 writer.reset(); 556 557 fragment.writeHtml( writer, this.htmlFilter ); 558 559 var data = writer.getHtml( true ); 560 561 // Restore those non-HTML protected source. (#4475,#4880) 562 data = unprotectRealComments( data ); 563 data = unprotectSource( data, this.editor ); 564 565 return data; 566 } 567 }; 568 })(); 569 570 /** 571 * Whether to force using "&" instead of "&" in elements attributes 572 * values, it's not recommended to change this setting for compliance with the 573 * W3C XHTML 1.0 standards (<a href="http://www.w3.org/TR/xhtml1/#C_12">C.12, XHTML 1.0</a>). 574 * @name CKEDITOR.config.forceSimpleAmpersand 575 * @name CKEDITOR.config.forceSimpleAmpersand 576 * @type Boolean 577 * @default false 578 * @example 579 * config.forceSimpleAmpersand = false; 580 */ 581 582 /** 583 * Whether a filler text (non-breaking space entity - ) will be inserted into empty block elements in HTML output, 584 * this is used to render block elements properly with line-height; When a function is instead specified, 585 * it'll be passed a {@link CKEDITOR.htmlParser.element} to decide whether adding the filler text 586 * by expecting a boolean return value. 587 * @name CKEDITOR.config.fillEmptyBlocks 588 * @since 3.5 589 * @type Boolean 590 * @default true 591 * @example 592 * config.fillEmptyBlocks = false; // Prevent filler nodes in all empty blocks. 593 * 594 * // Prevent filler node only in float cleaners. 595 * config.fillEmptyBlocks = function( element ) 596 * { 597 * if ( element.attributes[ 'class' ].indexOf ( 'clear-both' ) != -1 ) 598 * return false; 599 * } 600 */ 601