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 var fragmentPrototype = CKEDITOR.htmlParser.fragment.prototype, 9 elementPrototype = CKEDITOR.htmlParser.element.prototype; 10 11 fragmentPrototype.onlyChild = elementPrototype.onlyChild = function() 12 { 13 var children = this.children, 14 count = children.length, 15 firstChild = ( count == 1 ) && children[ 0 ]; 16 return firstChild || null; 17 }; 18 19 elementPrototype.removeAnyChildWithName = function( tagName ) 20 { 21 var children = this.children, 22 childs = [], 23 child; 24 25 for ( var i = 0; i < children.length; i++ ) 26 { 27 child = children[ i ]; 28 if ( !child.name ) 29 continue; 30 31 if ( child.name == tagName ) 32 { 33 childs.push( child ); 34 children.splice( i--, 1 ); 35 } 36 childs = childs.concat( child.removeAnyChildWithName( tagName ) ); 37 } 38 return childs; 39 }; 40 41 elementPrototype.getAncestor = function( tagNameRegex ) 42 { 43 var parent = this.parent; 44 while ( parent && !( parent.name && parent.name.match( tagNameRegex ) ) ) 45 parent = parent.parent; 46 return parent; 47 }; 48 49 fragmentPrototype.firstChild = elementPrototype.firstChild = function( evaluator ) 50 { 51 var child; 52 53 for ( var i = 0 ; i < this.children.length ; i++ ) 54 { 55 child = this.children[ i ]; 56 if ( evaluator( child ) ) 57 return child; 58 else if ( child.name ) 59 { 60 child = child.firstChild( evaluator ); 61 if ( child ) 62 return child; 63 } 64 } 65 66 return null; 67 }; 68 69 // Adding a (set) of styles to the element's 'style' attributes. 70 elementPrototype.addStyle = function( name, value, isPrepend ) 71 { 72 var styleText, addingStyleText = ''; 73 // name/value pair. 74 if ( typeof value == 'string' ) 75 addingStyleText += name + ':' + value + ';'; 76 else 77 { 78 // style literal. 79 if ( typeof name == 'object' ) 80 { 81 for ( var style in name ) 82 { 83 if ( name.hasOwnProperty( style ) ) 84 addingStyleText += style + ':' + name[ style ] + ';'; 85 } 86 } 87 // raw style text form. 88 else 89 addingStyleText += name; 90 91 isPrepend = value; 92 } 93 94 if ( !this.attributes ) 95 this.attributes = {}; 96 97 styleText = this.attributes.style || ''; 98 99 styleText = ( isPrepend ? 100 [ addingStyleText, styleText ] 101 : [ styleText, addingStyleText ] ).join( ';' ); 102 103 this.attributes.style = styleText.replace( /^;|;(?=;)/, '' ); 104 }; 105 106 /** 107 * Return the DTD-valid parent tag names of the specified one. 108 * @param tagName 109 */ 110 CKEDITOR.dtd.parentOf = function( tagName ) 111 { 112 var result = {}; 113 for ( var tag in this ) 114 { 115 if ( tag.indexOf( '$' ) == -1 && this[ tag ][ tagName ] ) 116 result[ tag ] = 1; 117 } 118 return result; 119 }; 120 121 // 1. move consistent list item styles up to list root. 122 // 2. clear out unnecessary list item numbering. 123 function postProcessList( list ) 124 { 125 var children = list.children, 126 child, 127 attrs, 128 count = list.children.length, 129 match, 130 mergeStyle, 131 styleTypeRegexp = /list-style-type:(.*?)(?:;|$)/, 132 stylesFilter = CKEDITOR.plugins.pastefromword.filters.stylesFilter; 133 134 attrs = list.attributes; 135 if ( styleTypeRegexp.exec( attrs.style ) ) 136 return; 137 138 for ( var i = 0; i < count; i++ ) 139 { 140 child = children[ i ]; 141 142 if ( child.attributes.value && Number( child.attributes.value ) == i + 1 ) 143 delete child.attributes.value; 144 145 match = styleTypeRegexp.exec( child.attributes.style ); 146 147 if ( match ) 148 { 149 if ( match[ 1 ] == mergeStyle || !mergeStyle ) 150 mergeStyle = match[ 1 ]; 151 else 152 { 153 mergeStyle = null; 154 break; 155 } 156 } 157 } 158 159 if ( mergeStyle ) 160 { 161 for ( i = 0; i < count; i++ ) 162 { 163 attrs = children[ i ].attributes; 164 attrs.style && ( attrs.style = stylesFilter( [ [ 'list-style-type'] ] )( attrs.style ) || '' ); 165 } 166 167 list.addStyle( 'list-style-type', mergeStyle ); 168 } 169 } 170 171 var cssLengthRelativeUnit = /^([.\d]*)+(em|ex|px|gd|rem|vw|vh|vm|ch|mm|cm|in|pt|pc|deg|rad|ms|s|hz|khz){1}?/i; 172 var emptyMarginRegex = /^(?:\b0[^\s]*\s*){1,4}$/; // e.g. 0px 0pt 0px 173 var romanLiternalPattern = '^m{0,4}(cm|cd|d?c{0,3})(xc|xl|l?x{0,3})(ix|iv|v?i{0,3})$', 174 lowerRomanLiteralRegex = new RegExp( romanLiternalPattern ), 175 upperRomanLiteralRegex = new RegExp( romanLiternalPattern.toUpperCase() ); 176 177 var orderedPatterns = { 'decimal' : /\d+/, 'lower-roman': lowerRomanLiteralRegex, 'upper-roman': upperRomanLiteralRegex, 'lower-alpha' : /^[a-z]+$/, 'upper-alpha': /^[A-Z]+$/ }, 178 unorderedPatterns = { 'disc' : /[l\u00B7\u2002]/, 'circle' : /[\u006F\u00D8]/,'square' : /[\u006E\u25C6]/}, 179 listMarkerPatterns = { 'ol' : orderedPatterns, 'ul' : unorderedPatterns }, 180 romans = [ [1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'], [100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'], [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I'] ], 181 alpahbets = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 182 183 // Convert roman numbering back to decimal. 184 function fromRoman( str ) 185 { 186 str = str.toUpperCase(); 187 var l = romans.length, retVal = 0; 188 for ( var i = 0; i < l; ++i ) 189 { 190 for ( var j = romans[i], k = j[1].length; str.substr( 0, k ) == j[1]; str = str.substr( k ) ) 191 retVal += j[ 0 ]; 192 } 193 return retVal; 194 } 195 196 // Convert alphabet numbering back to decimal. 197 function fromAlphabet( str ) 198 { 199 str = str.toUpperCase(); 200 var l = alpahbets.length, retVal = 1; 201 for ( var x = 1; str.length > 0; x *= l ) 202 { 203 retVal += alpahbets.indexOf( str.charAt( str.length - 1 ) ) * x; 204 str = str.substr( 0, str.length - 1 ); 205 } 206 return retVal; 207 } 208 209 var listBaseIndent = 0, 210 previousListItemMargin = null, 211 previousListId; 212 213 var plugin = ( CKEDITOR.plugins.pastefromword = 214 { 215 utils : 216 { 217 // Create a <cke:listbullet> which indicate an list item type. 218 createListBulletMarker : function ( bullet, bulletText ) 219 { 220 var marker = new CKEDITOR.htmlParser.element( 'cke:listbullet' ); 221 marker.attributes = { 'cke:listsymbol' : bullet[ 0 ] }; 222 marker.add( new CKEDITOR.htmlParser.text( bulletText ) ); 223 return marker; 224 }, 225 226 isListBulletIndicator : function( element ) 227 { 228 var styleText = element.attributes && element.attributes.style; 229 if ( /mso-list\s*:\s*Ignore/i.test( styleText ) ) 230 return true; 231 }, 232 233 isContainingOnlySpaces : function( element ) 234 { 235 var text; 236 return ( ( text = element.onlyChild() ) 237 && ( /^(:?\s| )+$/ ).test( text.value ) ); 238 }, 239 240 resolveList : function( element ) 241 { 242 // <cke:listbullet> indicate a list item. 243 var attrs = element.attributes, 244 listMarker; 245 246 if ( ( listMarker = element.removeAnyChildWithName( 'cke:listbullet' ) ) 247 && listMarker.length 248 && ( listMarker = listMarker[ 0 ] ) ) 249 { 250 element.name = 'cke:li'; 251 252 if ( attrs.style ) 253 { 254 attrs.style = plugin.filters.stylesFilter( 255 [ 256 // Text-indent is not representing list item level any more. 257 [ 'text-indent' ], 258 [ 'line-height' ], 259 // First attempt is to resolve indent level from on a constant margin increment. 260 [ ( /^margin(:?-left)?$/ ), null, function( margin ) 261 { 262 // Deal with component/short-hand form. 263 var values = margin.split( ' ' ); 264 margin = CKEDITOR.tools.convertToPx( values[ 3 ] || values[ 1 ] || values [ 0 ] ); 265 266 // Figure out the indent unit by checking the first time of incrementation. 267 if ( !listBaseIndent && previousListItemMargin !== null && margin > previousListItemMargin ) 268 listBaseIndent = margin - previousListItemMargin; 269 270 previousListItemMargin = margin; 271 272 attrs[ 'cke:indent' ] = listBaseIndent && ( Math.ceil( margin / listBaseIndent ) + 1 ) || 1; 273 } ], 274 // The best situation: "mso-list:l0 level1 lfo2" tells the belonged list root, list item indentation, etc. 275 [ ( /^mso-list$/ ), null, function( val ) 276 { 277 val = val.split( ' ' ); 278 var listId = Number( val[ 0 ].match( /\d+/ ) ), 279 indent = Number( val[ 1 ].match( /\d+/ ) ); 280 281 if ( indent == 1 ) 282 { 283 listId !== previousListId && ( attrs[ 'cke:reset' ] = 1 ); 284 previousListId = listId; 285 } 286 attrs[ 'cke:indent' ] = indent; 287 } ] 288 ] )( attrs.style, element ) || ''; 289 } 290 291 // First level list item might be presented without a margin. 292 293 294 // In case all above doesn't apply. 295 if ( !attrs[ 'cke:indent' ] ) 296 { 297 previousListItemMargin = 0; 298 attrs[ 'cke:indent' ] = 1; 299 } 300 301 // Inherit attributes from bullet. 302 CKEDITOR.tools.extend( attrs, listMarker.attributes ); 303 return true; 304 } 305 // Current list disconnected. 306 else 307 previousListId = previousListItemMargin = listBaseIndent = null; 308 309 return false; 310 }, 311 312 // Providing a shorthand style then retrieve one or more style component values. 313 getStyleComponents : ( function() 314 { 315 var calculator = CKEDITOR.dom.element.createFromHtml( 316 '<div style="position:absolute;left:-9999px;top:-9999px;"></div>', 317 CKEDITOR.document ); 318 CKEDITOR.document.getBody().append( calculator ); 319 320 return function( name, styleValue, fetchList ) 321 { 322 calculator.setStyle( name, styleValue ); 323 var styles = {}, 324 count = fetchList.length; 325 for ( var i = 0; i < count; i++ ) 326 styles[ fetchList[ i ] ] = calculator.getStyle( fetchList[ i ] ); 327 328 return styles; 329 }; 330 } )(), 331 332 listDtdParents : CKEDITOR.dtd.parentOf( 'ol' ) 333 }, 334 335 filters : 336 { 337 // Transform a normal list into flat list items only presentation. 338 // E.g. <ul><li>level1<ol><li>level2</li></ol></li> => 339 // <cke:li cke:listtype="ul" cke:indent="1">level1</cke:li> 340 // <cke:li cke:listtype="ol" cke:indent="2">level2</cke:li> 341 flattenList : function( element, level ) 342 { 343 level = typeof level == 'number' ? level : 1; 344 345 var attrs = element.attributes, 346 listStyleType; 347 348 // All list items are of the same type. 349 switch ( attrs.type ) 350 { 351 case 'a' : 352 listStyleType = 'lower-alpha'; 353 break; 354 case '1' : 355 listStyleType = 'decimal'; 356 break; 357 // TODO: Support more list style type from MS-Word. 358 } 359 360 var children = element.children, 361 child; 362 363 for ( var i = 0; i < children.length; i++ ) 364 { 365 child = children[ i ]; 366 367 if ( child.name in CKEDITOR.dtd.$listItem ) 368 { 369 var attributes = child.attributes, 370 listItemChildren = child.children, 371 count = listItemChildren.length, 372 last = listItemChildren[ count - 1 ]; 373 374 // Move out nested list. 375 if ( last.name in CKEDITOR.dtd.$list ) 376 { 377 element.add( last, i + 1 ); 378 379 // Remove the parent list item if it's just a holder. 380 if ( !--listItemChildren.length ) 381 children.splice( i--, 1 ); 382 } 383 384 child.name = 'cke:li'; 385 386 // Inherit numbering from list root on the first list item. 387 attrs.start && !i && ( attributes.value = attrs.start ); 388 389 plugin.filters.stylesFilter( 390 [ 391 [ 'tab-stops', null, function( val ) 392 { 393 var margin = val.split( ' ' )[ 1 ].match( cssLengthRelativeUnit ); 394 margin && ( previousListItemMargin = CKEDITOR.tools.convertToPx( margin[ 0 ] ) ); 395 } ], 396 ( level == 1 ? [ 'mso-list', null, function( val ) 397 { 398 val = val.split( ' ' ); 399 var listId = Number( val[ 0 ].match( /\d+/ ) ); 400 listId !== previousListId && ( attributes[ 'cke:reset' ] = 1 ); 401 previousListId = listId; 402 } ] : null ) 403 ] )( attributes.style ); 404 405 attributes[ 'cke:indent' ] = level; 406 attributes[ 'cke:listtype' ] = element.name; 407 attributes[ 'cke:list-style-type' ] = listStyleType; 408 } 409 // Flatten sub list. 410 else if ( child.name in CKEDITOR.dtd.$list ) 411 { 412 // Absorb sub list children. 413 arguments.callee.apply( this, [ child, level + 1 ] ); 414 children = children.slice( 0, i ).concat( child.children ).concat( children.slice( i + 1 ) ); 415 element.children = []; 416 for ( var j = 0, num = children.length; j < num ; j++ ) 417 element.add( children[ j ] ); 418 } 419 } 420 421 delete element.name; 422 423 // We're loosing tag name here, signalize this element as a list. 424 attrs[ 'cke:list' ] = 1; 425 }, 426 427 /** 428 * Try to collect all list items among the children and establish one 429 * or more HTML list structures for them. 430 * @param element 431 */ 432 assembleList : function( element ) 433 { 434 var children = element.children, child, 435 listItem, // The current processing cke:li element. 436 listItemAttrs, 437 listItemIndent, // Indent level of current list item. 438 lastIndent, 439 lastListItem, // The previous one just been added to the list. 440 list, // Current staging list and it's parent list if any. 441 openedLists = [], 442 previousListStyleType, 443 previousListType; 444 445 // Properties of the list item are to be resolved from the list bullet. 446 var bullet, 447 listType, 448 listStyleType, 449 itemNumeric; 450 451 for ( var i = 0; i < children.length; i++ ) 452 { 453 child = children[ i ]; 454 455 if ( 'cke:li' == child.name ) 456 { 457 child.name = 'li'; 458 listItem = child; 459 listItemAttrs = listItem.attributes; 460 bullet = listItemAttrs[ 'cke:listsymbol' ]; 461 bullet = bullet && bullet.match( /^(?:[(]?)([^\s]+?)([.)]?)$/ ); 462 listType = listStyleType = itemNumeric = null; 463 464 if ( listItemAttrs[ 'cke:ignored' ] ) 465 { 466 children.splice( i--, 1 ); 467 continue; 468 } 469 470 471 // This's from a new list root. 472 listItemAttrs[ 'cke:reset' ] && ( list = lastIndent = lastListItem = null ); 473 474 // List item indent level might come from a real list indentation or 475 // been resolved from a pseudo list item's margin value, even get 476 // no indentation at all. 477 listItemIndent = Number( listItemAttrs[ 'cke:indent' ] ); 478 479 // We're moving out of the current list, cleaning up. 480 if ( listItemIndent != lastIndent ) 481 previousListType = previousListStyleType = null; 482 483 // List type and item style are already resolved. 484 if ( !bullet ) 485 { 486 listType = listItemAttrs[ 'cke:listtype' ] || 'ol'; 487 listStyleType = listItemAttrs[ 'cke:list-style-type' ]; 488 } 489 else 490 { 491 // Probably share the same list style type with previous list item, 492 // give it priority to avoid ambiguous between C(Alpha) and C.(Roman). 493 if ( previousListType && listMarkerPatterns[ previousListType ] [ previousListStyleType ].test( bullet[ 1 ] ) ) 494 { 495 listType = previousListType; 496 listStyleType = previousListStyleType; 497 } 498 else 499 { 500 for ( var type in listMarkerPatterns ) 501 { 502 for ( var style in listMarkerPatterns[ type ] ) 503 { 504 if ( listMarkerPatterns[ type ][ style ].test( bullet[ 1 ] ) ) 505 { 506 // Small numbering has higher priority, when dealing with ambiguous 507 // between C(Alpha) and C.(Roman). 508 if ( type == 'ol' && ( /alpha|roman/ ).test( style ) ) 509 { 510 var num = /roman/.test( style ) ? fromRoman( bullet[ 1 ] ) : fromAlphabet( bullet[ 1 ] ); 511 if ( !itemNumeric || num < itemNumeric ) 512 { 513 itemNumeric = num; 514 listType = type; 515 listStyleType = style; 516 } 517 } 518 else 519 { 520 listType = type; 521 listStyleType = style; 522 break; 523 } 524 } 525 } 526 } 527 } 528 529 // Simply use decimal/disc for the rest forms of unrepresentable 530 // numerals, e.g. Chinese..., but as long as there a second part 531 // included, it has a bigger chance of being a order list ;) 532 !listType && ( listType = bullet[ 2 ] ? 'ol' : 'ul' ); 533 } 534 535 previousListType = listType; 536 previousListStyleType = listStyleType || ( listType == 'ol' ? 'decimal' : 'disc' ); 537 if ( listStyleType && listStyleType != ( listType == 'ol' ? 'decimal' : 'disc' ) ) 538 listItem.addStyle( 'list-style-type', listStyleType ); 539 540 // Figure out start numbering. 541 if ( listType == 'ol' && bullet ) 542 { 543 switch ( listStyleType ) 544 { 545 case 'decimal' : 546 itemNumeric = Number( bullet[ 1 ] ); 547 break; 548 case 'lower-roman': 549 case 'upper-roman': 550 itemNumeric = fromRoman( bullet[ 1 ] ); 551 break; 552 case 'lower-alpha': 553 case 'upper-alpha': 554 itemNumeric = fromAlphabet( bullet[ 1 ] ); 555 break; 556 } 557 558 // Always create the numbering, swipe out unnecessary ones later. 559 listItem.attributes.value = itemNumeric; 560 } 561 562 // Start the list construction. 563 if ( !list ) 564 { 565 openedLists.push( list = new CKEDITOR.htmlParser.element( listType ) ); 566 list.add( listItem ); 567 children[ i ] = list; 568 } 569 else 570 { 571 if ( listItemIndent > lastIndent ) 572 { 573 openedLists.push( list = new CKEDITOR.htmlParser.element( listType ) ); 574 list.add( listItem ); 575 lastListItem.add( list ); 576 } 577 else if ( listItemIndent < lastIndent ) 578 { 579 // There might be a negative gap between two list levels. (#4944) 580 var diff = lastIndent - listItemIndent, 581 parent; 582 while ( diff-- && ( parent = list.parent ) ) 583 list = parent.parent; 584 585 list.add( listItem ); 586 } 587 else 588 list.add( listItem ); 589 590 children.splice( i--, 1 ); 591 } 592 593 lastListItem = listItem; 594 lastIndent = listItemIndent; 595 } 596 else if ( list ) 597 list = lastIndent = lastListItem = null; 598 } 599 600 for ( i = 0; i < openedLists.length; i++ ) 601 postProcessList( openedLists[ i ] ); 602 603 list = lastIndent = lastListItem = previousListId = previousListItemMargin = listBaseIndent = null; 604 }, 605 606 /** 607 * A simple filter which always rejecting. 608 */ 609 falsyFilter : function( value ) 610 { 611 return false; 612 }, 613 614 /** 615 * A filter dedicated on the 'style' attribute filtering, e.g. dropping/replacing style properties. 616 * @param styles {Array} in form of [ styleNameRegexp, styleValueRegexp, 617 * newStyleValue/newStyleGenerator, newStyleName ] where only the first 618 * parameter is mandatory. 619 * @param whitelist {Boolean} Whether the {@param styles} will be considered as a white-list. 620 */ 621 stylesFilter : function( styles, whitelist ) 622 { 623 return function( styleText, element ) 624 { 625 var rules = []; 626 // html-encoded quote might be introduced by 'font-family' 627 // from MS-Word which confused the following regexp. e.g. 628 //'font-family: "Lucida, Console"' 629 ( styleText || '' ) 630 .replace( /"/g, '"' ) 631 .replace( /\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g, 632 function( match, name, value ) 633 { 634 name = name.toLowerCase(); 635 name == 'font-family' && ( value = value.replace( /["']/g, '' ) ); 636 637 var namePattern, 638 valuePattern, 639 newValue, 640 newName; 641 for ( var i = 0 ; i < styles.length; i++ ) 642 { 643 if ( styles[ i ] ) 644 { 645 namePattern = styles[ i ][ 0 ]; 646 valuePattern = styles[ i ][ 1 ]; 647 newValue = styles[ i ][ 2 ]; 648 newName = styles[ i ][ 3 ]; 649 650 if ( name.match( namePattern ) 651 && ( !valuePattern || value.match( valuePattern ) ) ) 652 { 653 name = newName || name; 654 whitelist && ( newValue = newValue || value ); 655 656 if ( typeof newValue == 'function' ) 657 newValue = newValue( value, element, name ); 658 659 // Return an couple indicate both name and value 660 // changed. 661 if ( newValue && newValue.push ) 662 name = newValue[ 0 ], newValue = newValue[ 1 ]; 663 664 if ( typeof newValue == 'string' ) 665 rules.push( [ name, newValue ] ); 666 return; 667 } 668 } 669 } 670 671 !whitelist && rules.push( [ name, value ] ); 672 673 }); 674 675 for ( var i = 0 ; i < rules.length ; i++ ) 676 rules[ i ] = rules[ i ].join( ':' ); 677 return rules.length ? 678 ( rules.join( ';' ) + ';' ) : false; 679 }; 680 }, 681 682 /** 683 * Migrate the element by decorate styles on it. 684 * @param styleDefiniton 685 * @param variables 686 */ 687 elementMigrateFilter : function ( styleDefiniton, variables ) 688 { 689 return function( element ) 690 { 691 var styleDef = 692 variables ? 693 new CKEDITOR.style( styleDefiniton, variables )._.definition 694 : styleDefiniton; 695 element.name = styleDef.element; 696 CKEDITOR.tools.extend( element.attributes, CKEDITOR.tools.clone( styleDef.attributes ) ); 697 element.addStyle( CKEDITOR.style.getStyleText( styleDef ) ); 698 }; 699 }, 700 701 /** 702 * Migrate styles by creating a new nested stylish element. 703 * @param styleDefinition 704 */ 705 styleMigrateFilter : function( styleDefinition, variableName ) 706 { 707 708 var elementMigrateFilter = this.elementMigrateFilter; 709 return function( value, element ) 710 { 711 // Build an stylish element first. 712 var styleElement = new CKEDITOR.htmlParser.element( null ), 713 variables = {}; 714 715 variables[ variableName ] = value; 716 elementMigrateFilter( styleDefinition, variables )( styleElement ); 717 // Place the new element inside the existing span. 718 styleElement.children = element.children; 719 element.children = [ styleElement ]; 720 }; 721 }, 722 723 /** 724 * A filter which remove cke-namespaced-attribute on 725 * all none-cke-namespaced elements. 726 * @param value 727 * @param element 728 */ 729 bogusAttrFilter : function( value, element ) 730 { 731 if ( element.name.indexOf( 'cke:' ) == -1 ) 732 return false; 733 }, 734 735 /** 736 * A filter which will be used to apply inline css style according the stylesheet 737 * definition rules, is generated lazily when filtering. 738 */ 739 applyStyleFilter : null 740 741 }, 742 743 getRules : function( editor ) 744 { 745 var dtd = CKEDITOR.dtd, 746 blockLike = CKEDITOR.tools.extend( {}, dtd.$block, dtd.$listItem, dtd.$tableContent ), 747 config = editor.config, 748 filters = this.filters, 749 falsyFilter = filters.falsyFilter, 750 stylesFilter = filters.stylesFilter, 751 elementMigrateFilter = filters.elementMigrateFilter, 752 styleMigrateFilter = CKEDITOR.tools.bind( this.filters.styleMigrateFilter, this.filters ), 753 createListBulletMarker = this.utils.createListBulletMarker, 754 flattenList = filters.flattenList, 755 assembleList = filters.assembleList, 756 isListBulletIndicator = this.utils.isListBulletIndicator, 757 containsNothingButSpaces = this.utils.isContainingOnlySpaces, 758 resolveListItem = this.utils.resolveList, 759 convertToPx = function( value ) 760 { 761 value = CKEDITOR.tools.convertToPx( value ); 762 return isNaN( value ) ? value : value + 'px'; 763 }, 764 getStyleComponents = this.utils.getStyleComponents, 765 listDtdParents = this.utils.listDtdParents, 766 removeFontStyles = config.pasteFromWordRemoveFontStyles !== false, 767 removeStyles = config.pasteFromWordRemoveStyles !== false; 768 769 return { 770 771 elementNames : 772 [ 773 // Remove script, meta and link elements. 774 [ ( /meta|link|script/ ), '' ] 775 ], 776 777 root : function( element ) 778 { 779 element.filterChildren(); 780 assembleList( element ); 781 }, 782 783 elements : 784 { 785 '^' : function( element ) 786 { 787 // Transform CSS style declaration to inline style. 788 var applyStyleFilter; 789 if ( CKEDITOR.env.gecko && ( applyStyleFilter = filters.applyStyleFilter ) ) 790 applyStyleFilter( element ); 791 }, 792 793 $ : function( element ) 794 { 795 var tagName = element.name || '', 796 attrs = element.attributes; 797 798 // Convert length unit of width/height on blocks to 799 // a more editor-friendly way (px). 800 if ( tagName in blockLike 801 && attrs.style ) 802 { 803 attrs.style = stylesFilter( 804 [ [ ( /^(:?width|height)$/ ), null, convertToPx ] ] )( attrs.style ) || ''; 805 } 806 807 // Processing headings. 808 if ( tagName.match( /h\d/ ) ) 809 { 810 element.filterChildren(); 811 // Is the heading actually a list item? 812 if ( resolveListItem( element ) ) 813 return; 814 815 // Adapt heading styles to editor's convention. 816 elementMigrateFilter( config[ 'format_' + tagName ] )( element ); 817 } 818 // Remove inline elements which contain only empty spaces. 819 else if ( tagName in dtd.$inline ) 820 { 821 element.filterChildren(); 822 if ( containsNothingButSpaces( element ) ) 823 delete element.name; 824 } 825 // Remove element with ms-office namespace, 826 // with it's content preserved, e.g. 'o:p'. 827 else if ( tagName.indexOf( ':' ) != -1 828 && tagName.indexOf( 'cke' ) == -1 ) 829 { 830 element.filterChildren(); 831 832 // Restore image real link from vml. 833 if ( tagName == 'v:imagedata' ) 834 { 835 var href = element.attributes[ 'o:href' ]; 836 if ( href ) 837 element.attributes.src = href; 838 element.name = 'img'; 839 return; 840 } 841 delete element.name; 842 } 843 844 // Assembling list items into a whole list. 845 if ( tagName in listDtdParents ) 846 { 847 element.filterChildren(); 848 assembleList( element ); 849 } 850 }, 851 852 // We'll drop any style sheet, but Firefox conclude 853 // certain styles in a single style element, which are 854 // required to be changed into inline ones. 855 'style' : function( element ) 856 { 857 if ( CKEDITOR.env.gecko ) 858 { 859 // Grab only the style definition section. 860 var styleDefSection = element.onlyChild().value.match( /\/\* Style Definitions \*\/([\s\S]*?)\/\*/ ), 861 styleDefText = styleDefSection && styleDefSection[ 1 ], 862 rules = {}; // Storing the parsed result. 863 864 if ( styleDefText ) 865 { 866 styleDefText 867 // Remove line-breaks. 868 .replace(/[\n\r]/g,'') 869 // Extract selectors and style properties. 870 .replace( /(.+?)\{(.+?)\}/g, 871 function( rule, selectors, styleBlock ) 872 { 873 selectors = selectors.split( ',' ); 874 var length = selectors.length, selector; 875 for ( var i = 0; i < length; i++ ) 876 { 877 // Assume MS-Word mostly generate only simple 878 // selector( [Type selector][Class selector]). 879 CKEDITOR.tools.trim( selectors[ i ] ) 880 .replace( /^(\w+)(\.[\w-]+)?$/g, 881 function( match, tagName, className ) 882 { 883 tagName = tagName || '*'; 884 className = className.substring( 1, className.length ); 885 886 // Reject MS-Word Normal styles. 887 if ( className.match( /MsoNormal/ ) ) 888 return; 889 890 if ( !rules[ tagName ] ) 891 rules[ tagName ] = {}; 892 if ( className ) 893 rules[ tagName ][ className ] = styleBlock; 894 else 895 rules[ tagName ] = styleBlock; 896 } ); 897 } 898 }); 899 900 filters.applyStyleFilter = function( element ) 901 { 902 var name = rules[ '*' ] ? '*' : element.name, 903 className = element.attributes && element.attributes[ 'class' ], 904 style; 905 if ( name in rules ) 906 { 907 style = rules[ name ]; 908 if ( typeof style == 'object' ) 909 style = style[ className ]; 910 // Maintain style rules priorities. 911 style && element.addStyle( style, true ); 912 } 913 }; 914 } 915 } 916 return false; 917 }, 918 919 'p' : function( element ) 920 { 921 // This's a fall-back approach to recognize list item in FF3.6, 922 // as it's not perfect as not all list style (e.g. "heading list") is shipped 923 // with this pattern. (#6662) 924 if ( /MsoListParagraph/.exec( element.attributes[ 'class' ] ) ) 925 { 926 var bulletText = element.firstChild( function( node ) 927 { 928 return node.type == CKEDITOR.NODE_TEXT && !containsNothingButSpaces( node.parent ); 929 }); 930 var bullet = bulletText && bulletText.parent, 931 bulletAttrs = bullet && bullet.attributes; 932 bulletAttrs && !bulletAttrs.style && ( bulletAttrs.style = 'mso-list: Ignore;' ); 933 } 934 935 element.filterChildren(); 936 937 // Is the paragraph actually a list item? 938 if ( resolveListItem( element ) ) 939 return; 940 941 // Adapt paragraph formatting to editor's convention 942 // according to enter-mode. 943 if ( config.enterMode == CKEDITOR.ENTER_BR ) 944 { 945 // We suffer from attribute/style lost in this situation. 946 delete element.name; 947 element.add( new CKEDITOR.htmlParser.element( 'br' ) ); 948 } 949 else 950 elementMigrateFilter( config[ 'format_' + ( config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) ] )( element ); 951 }, 952 953 'div' : function( element ) 954 { 955 // Aligned table with no text surrounded is represented by a wrapper div, from which 956 // table cells inherit as text-align styles, which is wrong. 957 // Instead we use a clear-float div after the table to properly achieve the same layout. 958 var singleChild = element.onlyChild(); 959 if ( singleChild && singleChild.name == 'table' ) 960 { 961 var attrs = element.attributes; 962 singleChild.attributes = CKEDITOR.tools.extend( singleChild.attributes, attrs ); 963 attrs.style && singleChild.addStyle( attrs.style ); 964 965 var clearFloatDiv = new CKEDITOR.htmlParser.element( 'div' ); 966 clearFloatDiv.addStyle( 'clear' ,'both' ); 967 element.add( clearFloatDiv ); 968 delete element.name; 969 } 970 }, 971 972 'td' : function ( element ) 973 { 974 // 'td' in 'thead' is actually <th>. 975 if ( element.getAncestor( 'thead') ) 976 element.name = 'th'; 977 }, 978 979 // MS-Word sometimes present list as a mixing of normal list 980 // and pseudo-list, normalize the previous ones into pseudo form. 981 'ol' : flattenList, 982 'ul' : flattenList, 983 'dl' : flattenList, 984 985 'font' : function( element ) 986 { 987 // Drop the font tag if it comes from list bullet text. 988 if ( isListBulletIndicator( element.parent ) ) 989 { 990 delete element.name; 991 return; 992 } 993 994 element.filterChildren(); 995 996 var attrs = element.attributes, 997 styleText = attrs.style, 998 parent = element.parent; 999 1000 if ( 'font' == parent.name ) // Merge nested <font> tags. 1001 { 1002 CKEDITOR.tools.extend( parent.attributes, 1003 element.attributes ); 1004 styleText && parent.addStyle( styleText ); 1005 delete element.name; 1006 } 1007 // Convert the merged into a span with all attributes preserved. 1008 else 1009 { 1010 styleText = styleText || ''; 1011 // IE's having those deprecated attributes, normalize them. 1012 if ( attrs.color ) 1013 { 1014 attrs.color != '#000000' && ( styleText += 'color:' + attrs.color + ';' ); 1015 delete attrs.color; 1016 } 1017 if ( attrs.face ) 1018 { 1019 styleText += 'font-family:' + attrs.face + ';'; 1020 delete attrs.face; 1021 } 1022 // TODO: Mapping size in ranges of xx-small, 1023 // x-small, small, medium, large, x-large, xx-large. 1024 if ( attrs.size ) 1025 { 1026 styleText += 'font-size:' + 1027 ( attrs.size > 3 ? 'large' 1028 : ( attrs.size < 3 ? 'small' : 'medium' ) ) + ';'; 1029 delete attrs.size; 1030 } 1031 1032 element.name = 'span'; 1033 element.addStyle( styleText ); 1034 } 1035 }, 1036 1037 'span' : function( element ) 1038 { 1039 // Remove the span if it comes from list bullet text. 1040 if ( isListBulletIndicator( element.parent ) ) 1041 return false; 1042 1043 element.filterChildren(); 1044 if ( containsNothingButSpaces( element ) ) 1045 { 1046 delete element.name; 1047 return null; 1048 } 1049 1050 // List item bullet type is supposed to be indicated by 1051 // the text of a span with style 'mso-list : Ignore' or an image. 1052 if ( isListBulletIndicator( element ) ) 1053 { 1054 var listSymbolNode = element.firstChild( function( node ) 1055 { 1056 return node.value || node.name == 'img'; 1057 }); 1058 1059 var listSymbol = listSymbolNode && ( listSymbolNode.value || 'l.' ), 1060 listType = listSymbol && listSymbol.match( /^(?:[(]?)([^\s]+?)([.)]?)$/ ); 1061 1062 if ( listType ) 1063 { 1064 var marker = createListBulletMarker( listType, listSymbol ); 1065 // Some non-existed list items might be carried by an inconsequential list, indicate by "mso-hide:all/display:none", 1066 // those are to be removed later, now mark it with "cke:ignored". 1067 var ancestor = element.getAncestor( 'span' ); 1068 if ( ancestor && (/ mso-hide:\s*all|display:\s*none /).test( ancestor.attributes.style ) ) 1069 marker.attributes[ 'cke:ignored' ] = 1; 1070 return marker; 1071 } 1072 } 1073 1074 // Update the src attribute of image element with href. 1075 var children = element.children, 1076 attrs = element.attributes, 1077 styleText = attrs && attrs.style, 1078 firstChild = children && children[ 0 ]; 1079 1080 // Assume MS-Word mostly carry font related styles on <span>, 1081 // adapting them to editor's convention. 1082 if ( styleText ) 1083 { 1084 attrs.style = stylesFilter( 1085 [ 1086 // Drop 'inline-height' style which make lines overlapping. 1087 [ 'line-height' ], 1088 [ ( /^font-family$/ ), null, !removeFontStyles ? styleMigrateFilter( config[ 'font_style' ], 'family' ) : null ] , 1089 [ ( /^font-size$/ ), null, !removeFontStyles ? styleMigrateFilter( config[ 'fontSize_style' ], 'size' ) : null ] , 1090 [ ( /^color$/ ), null, !removeFontStyles ? styleMigrateFilter( config[ 'colorButton_foreStyle' ], 'color' ) : null ] , 1091 [ ( /^background-color$/ ), null, !removeFontStyles ? styleMigrateFilter( config[ 'colorButton_backStyle' ], 'color' ) : null ] 1092 ] )( styleText, element ) || ''; 1093 } 1094 1095 return null; 1096 }, 1097 1098 // Migrate basic style formats to editor configured ones. 1099 'b' : elementMigrateFilter( config[ 'coreStyles_bold' ] ), 1100 'i' : elementMigrateFilter( config[ 'coreStyles_italic' ] ), 1101 'u' : elementMigrateFilter( config[ 'coreStyles_underline' ] ), 1102 's' : elementMigrateFilter( config[ 'coreStyles_strike' ] ), 1103 'sup' : elementMigrateFilter( config[ 'coreStyles_superscript' ] ), 1104 'sub' : elementMigrateFilter( config[ 'coreStyles_subscript' ] ), 1105 // Editor doesn't support anchor with content currently (#3582), 1106 // drop such anchors with content preserved. 1107 'a' : function( element ) 1108 { 1109 var attrs = element.attributes; 1110 if ( attrs && !attrs.href && attrs.name ) 1111 delete element.name; 1112 else if ( CKEDITOR.env.webkit && attrs.href && attrs.href.match( /file:\/\/\/[\S]+#/i ) ) 1113 attrs.href = attrs.href.replace( /file:\/\/\/[^#]+/i,'' ); 1114 }, 1115 'cke:listbullet' : function( element ) 1116 { 1117 if ( element.getAncestor( /h\d/ ) && !config.pasteFromWordNumberedHeadingToList ) 1118 delete element.name; 1119 } 1120 }, 1121 1122 attributeNames : 1123 [ 1124 // Remove onmouseover and onmouseout events (from MS Word comments effect) 1125 [ ( /^onmouse(:?out|over)/ ), '' ], 1126 // Onload on image element. 1127 [ ( /^onload$/ ), '' ], 1128 // Remove office and vml attribute from elements. 1129 [ ( /(?:v|o):\w+/ ), '' ], 1130 // Remove lang/language attributes. 1131 [ ( /^lang/ ), '' ] 1132 ], 1133 1134 attributes : 1135 { 1136 'style' : stylesFilter( 1137 removeStyles ? 1138 // Provide a white-list of styles that we preserve, those should 1139 // be the ones that could later be altered with editor tools. 1140 [ 1141 // Leave list-style-type 1142 [ ( /^list-style-type$/ ), null ], 1143 1144 // Preserve margin-left/right which used as default indent style in the editor. 1145 [ ( /^margin$|^margin-(?!bottom|top)/ ), null, function( value, element, name ) 1146 { 1147 if ( element.name in { p : 1, div : 1 } ) 1148 { 1149 var indentStyleName = config.contentsLangDirection == 'ltr' ? 1150 'margin-left' : 'margin-right'; 1151 1152 // Extract component value from 'margin' shorthand. 1153 if ( name == 'margin' ) 1154 { 1155 value = getStyleComponents( name, value, 1156 [ indentStyleName ] )[ indentStyleName ]; 1157 } 1158 else if ( name != indentStyleName ) 1159 return null; 1160 1161 if ( value && !emptyMarginRegex.test( value ) ) 1162 return [ indentStyleName, value ]; 1163 } 1164 1165 return null; 1166 } ], 1167 1168 // Preserve clear float style. 1169 [ ( /^clear$/ ) ], 1170 1171 [ ( /^border.*|margin.*|vertical-align|float$/ ), null, 1172 function( value, element ) 1173 { 1174 if ( element.name == 'img' ) 1175 return value; 1176 } ], 1177 1178 [ (/^width|height$/ ), null, 1179 function( value, element ) 1180 { 1181 if ( element.name in { table : 1, td : 1, th : 1, img : 1 } ) 1182 return value; 1183 } ] 1184 ] : 1185 // Otherwise provide a black-list of styles that we remove. 1186 [ 1187 [ ( /^mso-/ ) ], 1188 // Fixing color values. 1189 [ ( /-color$/ ), null, function( value ) 1190 { 1191 if ( value == 'transparent' ) 1192 return false; 1193 if ( CKEDITOR.env.gecko ) 1194 return value.replace( /-moz-use-text-color/g, 'transparent' ); 1195 } ], 1196 // Remove empty margin values, e.g. 0.00001pt 0em 0pt 1197 [ ( /^margin$/ ), emptyMarginRegex ], 1198 [ 'text-indent', '0cm' ], 1199 [ 'page-break-before' ], 1200 [ 'tab-stops' ], 1201 [ 'display', 'none' ], 1202 removeFontStyles ? [ ( /font-?/ ) ] : null 1203 ], removeStyles ), 1204 1205 // Prefer width styles over 'width' attributes. 1206 'width' : function( value, element ) 1207 { 1208 if ( element.name in dtd.$tableContent ) 1209 return false; 1210 }, 1211 // Prefer border styles over table 'border' attributes. 1212 'border' : function( value, element ) 1213 { 1214 if ( element.name in dtd.$tableContent ) 1215 return false; 1216 }, 1217 1218 // Only Firefox carry style sheet from MS-Word, which 1219 // will be applied by us manually. For other browsers 1220 // the css className is useless. 1221 'class' : falsyFilter, 1222 1223 // MS-Word always generate 'background-color' along with 'bgcolor', 1224 // simply drop the deprecated attributes. 1225 'bgcolor' : falsyFilter, 1226 1227 // Deprecate 'valign' attribute in favor of 'vertical-align'. 1228 'valign' : removeStyles ? falsyFilter : function( value, element ) 1229 { 1230 element.addStyle( 'vertical-align', value ); 1231 return false; 1232 } 1233 }, 1234 1235 // Fore none-IE, some useful data might be buried under these IE-conditional 1236 // comments where RegExp were the right approach to dig them out where usual approach 1237 // is transform it into a fake element node which hold the desired data. 1238 comment : 1239 !CKEDITOR.env.ie ? 1240 function( value, node ) 1241 { 1242 var imageInfo = value.match( /<img.*?>/ ), 1243 listInfo = value.match( /^\[if !supportLists\]([\s\S]*?)\[endif\]$/ ); 1244 1245 // Seek for list bullet indicator. 1246 if ( listInfo ) 1247 { 1248 // Bullet symbol could be either text or an image. 1249 var listSymbol = listInfo[ 1 ] || ( imageInfo && 'l.' ), 1250 listType = listSymbol && listSymbol.match( />(?:[(]?)([^\s]+?)([.)]?)</ ); 1251 return createListBulletMarker( listType, listSymbol ); 1252 } 1253 1254 // Reveal the <img> element in conditional comments for Firefox. 1255 if ( CKEDITOR.env.gecko && imageInfo ) 1256 { 1257 var img = CKEDITOR.htmlParser.fragment.fromHtml( imageInfo[ 0 ] ).children[ 0 ], 1258 previousComment = node.previous, 1259 // Try to dig the real image link from vml markup from previous comment text. 1260 imgSrcInfo = previousComment && previousComment.value.match( /<v:imagedata[^>]*o:href=['"](.*?)['"]/ ), 1261 imgSrc = imgSrcInfo && imgSrcInfo[ 1 ]; 1262 1263 // Is there a real 'src' url to be used? 1264 imgSrc && ( img.attributes.src = imgSrc ); 1265 return img; 1266 } 1267 1268 return false; 1269 } 1270 : falsyFilter 1271 }; 1272 } 1273 }); 1274 1275 // The paste processor here is just a reduced copy of html data processor. 1276 var pasteProcessor = function() 1277 { 1278 this.dataFilter = new CKEDITOR.htmlParser.filter(); 1279 }; 1280 1281 pasteProcessor.prototype = 1282 { 1283 toHtml : function( data ) 1284 { 1285 var fragment = CKEDITOR.htmlParser.fragment.fromHtml( data, false ), 1286 writer = new CKEDITOR.htmlParser.basicWriter(); 1287 1288 fragment.writeHtml( writer, this.dataFilter ); 1289 return writer.getHtml( true ); 1290 } 1291 }; 1292 1293 CKEDITOR.cleanWord = function( data, editor ) 1294 { 1295 // Firefox will be confused by those downlevel-revealed IE conditional 1296 // comments, fixing them first( convert it to upperlevel-revealed one ). 1297 // e.g. <![if !vml]>...<![endif]> 1298 if ( CKEDITOR.env.gecko ) 1299 data = data.replace( /(<!--\[if[^<]*?\])-->([\S\s]*?)<!--(\[endif\]-->)/gi, '$1$2$3' ); 1300 1301 var dataProcessor = new pasteProcessor(), 1302 dataFilter = dataProcessor.dataFilter; 1303 1304 // These rules will have higher priorities than default ones. 1305 dataFilter.addRules( CKEDITOR.plugins.pastefromword.getRules( editor ) ); 1306 1307 // Allow extending data filter rules. 1308 editor.fire( 'beforeCleanWord', { filter : dataFilter } ); 1309 1310 try 1311 { 1312 data = dataProcessor.toHtml( data, false ); 1313 } 1314 catch ( e ) 1315 { 1316 alert( editor.lang.pastefromword.error ); 1317 } 1318 1319 /* Below post processing those things that are unable to delivered by filter rules. */ 1320 1321 // Remove 'cke' namespaced attribute used in filter rules as marker. 1322 data = data.replace( /cke:.*?".*?"/g, '' ); 1323 1324 // Remove empty style attribute. 1325 data = data.replace( /style=""/g, '' ); 1326 1327 // Remove the dummy spans ( having no inline style ). 1328 data = data.replace( /<span>/g, '' ); 1329 1330 return data; 1331 }; 1332 })(); 1333 1334 /** 1335 * Whether to ignore all font related formatting styles, including: 1336 * <ul> <li>font size;</li> 1337 * <li>font family;</li> 1338 * <li>font foreground/background color.</li></ul> 1339 * @name CKEDITOR.config.pasteFromWordRemoveFontStyles 1340 * @since 3.1 1341 * @type Boolean 1342 * @default true 1343 * @example 1344 * config.pasteFromWordRemoveFontStyles = false; 1345 */ 1346 1347 /** 1348 * Whether to transform MS Word outline numbered headings into lists. 1349 * @name CKEDITOR.config.pasteFromWordNumberedHeadingToList 1350 * @since 3.1 1351 * @type Boolean 1352 * @default false 1353 * @example 1354 * config.pasteFromWordNumberedHeadingToList = true; 1355 */ 1356 1357 /** 1358 * Whether to remove element styles that can't be managed with the editor. Note 1359 * that this doesn't handle the font specific styles, which depends on the 1360 * {@link CKEDITOR.config.pasteFromWordRemoveFontStyles} setting instead. 1361 * @name CKEDITOR.config.pasteFromWordRemoveStyles 1362 * @since 3.1 1363 * @type Boolean 1364 * @default true 1365 * @example 1366 * config.pasteFromWordRemoveStyles = false; 1367 */ 1368