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