D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
home
/
shubmkcj
/
designpitara.com
/
admin
/
ckeditor
/
plugins
/
pastefromword
/
filter
/
Filename :
default.js
back
Copy
/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /* globals CKEDITOR */ ( function() { 'use strict'; var tools = CKEDITOR.tools, pastetools = CKEDITOR.plugins.pastetools, commonFilter = pastetools.filters.common, Style = commonFilter.styles, createAttributeStack = commonFilter.createAttributeStack, getElementIndentation = commonFilter.lists.getElementIndentation, invalidTags = [ 'o:p', 'xml', 'script', 'meta', 'link' ], shapeTags = [ 'v:arc', 'v:curve', 'v:line', 'v:oval', 'v:polyline', 'v:rect', 'v:roundrect', 'v:group' ], links = {}, inComment = 0, plug = {}, List, Heuristics; /** * Set of Paste from Word plugin helpers. * * @since 4.13.0 * @private * @member CKEDITOR.plugins.pastetools.filters */ CKEDITOR.plugins.pastetools.filters.word = plug; /** * Set of Paste from Word plugin helpers. * * See {@link CKEDITOR.plugins.pastetools.filters.word}. * * @since 4.6.0 * @deprecated 4.13.0 * @private * @member CKEDITOR.plugins */ CKEDITOR.plugins.pastefromword = plug; /** * Rules for the Paste from Word filter. * * @since 4.13.0 * @private * @member CKEDITOR.plugins.pastetools.filters.word */ plug.rules = function( html, editor, filter ) { var msoListsDetected = Boolean( html.match( /mso-list:\s*l\d+\s+level\d+\s+lfo\d+/ ) ), shapesIds = [], rules = { root: function( element ) { element.filterChildren( filter ); CKEDITOR.plugins.pastefromword.lists.cleanup( List.createLists( element ) ); }, elementNames: [ [ ( /^\?xml:namespace$/ ), '' ], [ /^v:shapetype/, '' ], [ new RegExp( invalidTags.join( '|' ) ), '' ] // Remove invalid tags. ], elements: { 'a': function( element ) { // Redundant anchor created by IE8. if ( element.attributes.name ) { if ( element.attributes.name == '_GoBack' ) { delete element.name; return; } // Garbage links that go nowhere. if ( element.attributes.name.match( /^OLE_LINK\d+$/ ) ) { delete element.name; return; } } if ( element.attributes.href && element.attributes.href.match( /#.+$/ ) ) { var name = element.attributes.href.match( /#(.+)$/ )[ 1 ]; links[ name ] = element; } if ( element.attributes.name && links[ element.attributes.name ] ) { var link = links[ element.attributes.name ]; link.attributes.href = link.attributes.href.replace( /.*#(.*)$/, '#$1' ); } }, 'div': function( element ) { // Don't allow to delete page break element (#3220). if ( editor.plugins.pagebreak && element.attributes[ 'data-cke-pagebreak' ] ) { return element; } Style.createStyleStack( element, filter, editor ); }, 'img': function( element ) { // If the parent is DocumentFragment it does not have any attributes. (https://dev.ckeditor.com/ticket/16912) if ( element.parent && element.parent.attributes ) { var attrs = element.parent.attributes, style = attrs.style || attrs.STYLE; if ( style && style.match( /mso\-list:\s?Ignore/ ) ) { element.attributes[ 'cke-ignored' ] = true; } } Style.mapCommonStyles( element ); if ( element.attributes.src && element.attributes.src.match( /^file:\/\// ) && element.attributes.alt && element.attributes.alt.match( /^https?:\/\// ) ) { element.attributes.src = element.attributes.alt; } var imgShapesIds = element.attributes[ 'v:shapes' ] ? element.attributes[ 'v:shapes' ].split( ' ' ) : []; // Check whether attribute contains shapes recognised earlier (stored in global list of shapesIds). // If so, add additional data-attribute to img tag. var isShapeFromList = CKEDITOR.tools.array.every( imgShapesIds, function( shapeId ) { return shapesIds.indexOf( shapeId ) > -1; } ); if ( imgShapesIds.length && isShapeFromList ) { // As we don't know how to process shapes we can remove them. return false; } }, 'p': function( element ) { element.filterChildren( filter ); if ( element.attributes.style && element.attributes.style.match( /display:\s*none/i ) ) { return false; } if ( List.thisIsAListItem( editor, element ) ) { if ( Heuristics.isEdgeListItem( editor, element ) ) { Heuristics.cleanupEdgeListItem( element ); } List.convertToFakeListItem( editor, element ); // IE pastes nested paragraphs in list items, which is different from other browsers. (https://dev.ckeditor.com/ticket/16826) // There's a possibility that list item will contain multiple paragraphs, in that case we want // to split them with BR. tools.array.reduce( element.children, function( paragraphsReplaced, node ) { if ( node.name === 'p' ) { // If there were already paragraphs replaced, put a br before this paragraph, so that // it's inline children are displayed in a next line. if ( paragraphsReplaced > 0 ) { var br = new CKEDITOR.htmlParser.element( 'br' ); br.insertBefore( node ); } node.replaceWithChildren(); paragraphsReplaced += 1; } return paragraphsReplaced; }, 0 ); } else { // In IE list level information is stored in <p> elements inside <li> elements. var container = element.getAscendant( function( element ) { return element.name == 'ul' || element.name == 'ol'; } ), style = tools.parseCssText( element.attributes.style ); if ( container && !container.attributes[ 'cke-list-level' ] && style[ 'mso-list' ] && style[ 'mso-list' ].match( /level/ ) ) { container.attributes[ 'cke-list-level' ] = style[ 'mso-list' ].match( /level(\d+)/ )[1]; } // Adapt paragraph formatting to editor's convention according to enter-mode (#423). if ( editor.config.enterMode == CKEDITOR.ENTER_BR ) { // We suffer from attribute/style lost in this situation. delete element.name; element.add( new CKEDITOR.htmlParser.element( 'br' ) ); } } Style.createStyleStack( element, filter, editor ); }, 'pre': function( element ) { if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element ); Style.createStyleStack( element, filter, editor ); }, 'h1': function( element ) { if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element ); Style.createStyleStack( element, filter, editor ); }, 'h2': function( element ) { if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element ); Style.createStyleStack( element, filter, editor ); }, 'h3': function( element ) { if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element ); Style.createStyleStack( element, filter, editor ); }, 'h4': function( element ) { if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element ); Style.createStyleStack( element, filter, editor ); }, 'h5': function( element ) { if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element ); Style.createStyleStack( element, filter, editor ); }, 'h6': function( element ) { if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element ); Style.createStyleStack( element, filter, editor ); }, 'font': function( element ) { if ( element.getHtml().match( /^\s*$/ ) ) { // There might be font tag directly in document fragment, we cannot replace it with a textnode as this generates // superfluous spaces in output. What later might be transformed into empty paragraphs, so just remove such element. if ( element.parent.type === CKEDITOR.NODE_ELEMENT ) { new CKEDITOR.htmlParser.text( ' ' ).insertAfter( element ); } return false; } if ( editor && editor.config.pasteFromWordRemoveFontStyles === true && element.attributes.size ) { // font[size] are still used by old IEs for font size. delete element.attributes.size; } // Create style stack for td/th > font if only class // and style attributes are present. Such markup is produced by Excel. if ( CKEDITOR.dtd.tr[ element.parent.name ] && CKEDITOR.tools.arrayCompare( CKEDITOR.tools.object.keys( element.attributes ), [ 'class', 'style' ] ) ) { Style.createStyleStack( element, filter, editor ); } else { createAttributeStack( element, filter ); } }, 'ul': function( element ) { if ( !msoListsDetected ) { // List should only be processed if we're sure we're working with Word. (https://dev.ckeditor.com/ticket/16593) return; } // Edge case from 11683 - an unusual way to create a level 2 list. if ( element.parent.name == 'li' && tools.indexOf( element.parent.children, element ) === 0 ) { Style.setStyle( element.parent, 'list-style-type', 'none' ); } List.dissolveList( element ); return false; }, 'li': function( element ) { Heuristics.correctLevelShift( element ); if ( !msoListsDetected ) { return; } element.attributes.style = Style.normalizedStyles( element, editor ); Style.pushStylesLower( element ); }, 'ol': function( element ) { if ( !msoListsDetected ) { // List should only be processed if we're sure we're working with Word. (https://dev.ckeditor.com/ticket/16593) return; } // Fix edge-case where when a list skips a level in IE11, the <ol> element // is implicitly surrounded by a <li>. if ( element.parent.name == 'li' && tools.indexOf( element.parent.children, element ) === 0 ) { Style.setStyle( element.parent, 'list-style-type', 'none' ); } List.dissolveList( element ); return false; }, 'span': function( element ) { element.filterChildren( filter ); element.attributes.style = Style.normalizedStyles( element, editor ); if ( !element.attributes.style || // Remove garbage bookmarks that disrupt the content structure. element.attributes.style.match( /^mso\-bookmark:OLE_LINK\d+$/ ) || element.getHtml().match( /^(\s| )+$/ ) ) { commonFilter.elements.replaceWithChildren( element ); return false; } if ( element.attributes.style.match( /FONT-FAMILY:\s*Symbol/i ) ) { element.forEach( function( node ) { node.value = node.value.replace( / /g, '' ); }, CKEDITOR.NODE_TEXT, true ); } Style.createStyleStack( element, filter, editor ); }, 'v:imagedata': remove, // This is how IE8 presents images. 'v:shape': function( element ) { // There are 3 paths: // 1. There is regular `v:shape` (no `v:imagedata` inside). // 2. There is a simple situation with `v:shape` with `v:imagedata` inside. We can remove such element and rely on `img` tag found later on. // 3. There is a complicated situation where we cannot find proper `img` tag after `v:shape` or there is some canvas element. // a) If shape is a child of v:group, then most probably it belongs to canvas, so we need to treat it as in path 1. // b) In other cases, most probably there is no related `img` tag. We need to transform `v:shape` into `img` tag (IE8 integration). var duplicate = false, child = element.getFirst( 'v:imagedata' ); // Path 1: if ( child === null ) { shapeTagging( element ); return; } // Path 2: // Sometimes a child with proper ID might be nested in other tag. element.parent.find( function( child ) { if ( child.name == 'img' && child.attributes && child.attributes[ 'v:shapes' ] == element.attributes.id ) { duplicate = true; } }, true ); if ( duplicate ) { return false; } else { // Path 3: var src = ''; // 3.a) Filter out situation when canvas is used. In such scenario there is v:group containing v:shape containing v:imagedata. // We streat such v:shapes as in Path 1. if ( element.parent.name === 'v:group' ) { shapeTagging( element ); return; } // 3.b) Most probably there is no img tag later on, so we need to transform this v:shape into img. This should only happen on IE8. element.forEach( function( child ) { if ( child.attributes && child.attributes.src ) { src = child.attributes.src; } }, CKEDITOR.NODE_ELEMENT, true ); element.filterChildren( filter ); element.name = 'img'; element.attributes.src = element.attributes.src || src; delete element.attributes.type; } return; }, 'style': function() { // We don't want to let any styles in. Firefox tends to add some. return false; }, 'object': function( element ) { // The specs about object `data` attribute: // Address of the resource as a valid URL. At least one of data and type must be defined. // If there is not `data`, skip the object element. (https://dev.ckeditor.com/ticket/17001) return !!( element.attributes && element.attributes.data ); }, // Integrate page breaks with `pagebreak` plugin (#2598). 'br': function( element ) { if ( !editor.plugins.pagebreak ) { return; } var styles = tools.parseCssText( element.attributes.style, true ); // Safari uses `break-before` instead of `page-break-before` to recognize page breaks. if ( styles[ 'page-break-before' ] === 'always' || styles[ 'break-before' ] === 'page' ) { var pagebreakEl = CKEDITOR.plugins.pagebreak.createElement( editor ); return CKEDITOR.htmlParser.fragment.fromHtml( pagebreakEl.getOuterHtml() ).children[ 0 ]; } } }, attributes: { 'style': function( styles, element ) { // Returning false deletes the attribute. return Style.normalizedStyles( element, editor ) || false; }, 'class': function( classes ) { // The (el\d+)|(font\d+) are default Excel classes for table cells and text. return falseIfEmpty( classes.replace( /(el\d+)|(font\d+)|msonormal|msolistparagraph\w*/ig, '' ) ); }, 'cellspacing': remove, 'cellpadding': remove, 'border': remove, 'v:shapes': remove, 'o:spid': remove }, comment: function( element ) { if ( element.match( /\[if.* supportFields.*\]/ ) ) { inComment++; } if ( element == '[endif]' ) { inComment = inComment > 0 ? inComment - 1 : 0; } return false; }, text: function( content, node ) { if ( inComment ) { return ''; } var grandparent = node.parent && node.parent.parent; if ( grandparent && grandparent.attributes && grandparent.attributes.style && grandparent.attributes.style.match( /mso-list:\s*ignore/i ) ) { return content.replace( / /g, ' ' ); } return content; } }; tools.array.forEach( shapeTags, function( shapeTag ) { rules.elements[ shapeTag ] = shapeTagging; } ); return rules; function shapeTagging( element ) { // Check if regular or canvas shape (#1088). if ( element.attributes[ 'o:gfxdata' ] || element.parent.name === 'v:group' ) { shapesIds.push( element.attributes.id ); } } }; /** * Namespace containing list-oriented helper methods. * * @private * @since 4.13.0 * @member CKEDITOR.plugins.pastetools.filters.word */ plug.lists = { /** * Checks if a given element is a list item-alike. * * @private * @since 4.13.0 * @param {CKEDITOR.editor} editor * @param {CKEDITOR.htmlParser.element} element * @returns {Boolean} * @member CKEDITOR.plugins.pastetools.filters.word.lists */ thisIsAListItem: function( editor, element ) { if ( Heuristics.isEdgeListItem( editor, element ) ) { return true; } /*jshint -W024 */ // Normally a style of the sort that looks like "mso-list: l0 level1 lfo1" // indicates a list element, but the same style may appear in a <p> that's within a <li>. if ( ( element.attributes.style && element.attributes.style.match( /mso\-list:\s?l\d/ ) && element.parent.name !== 'li' ) || element.attributes[ 'cke-dissolved' ] || element.getHtml().match( /<!\-\-\[if !supportLists]\-\->/ ) ) { return true; } return false; /*jshint +W024 */ }, /** * Converts an element to an element with the `cke:li` tag name. * * @private * @since 4.13.0 * @param {CKEDITOR.editor} editor * @param {CKEDITOR.htmlParser.element} element * @member CKEDITOR.plugins.pastetools.filters.word.lists */ convertToFakeListItem: function( editor, element ) { if ( Heuristics.isDegenerateListItem( editor, element ) ) { Heuristics.assignListLevels( editor, element ); } // A dummy call to cache parsed list info inside of cke-list-* attributes. this.getListItemInfo( element ); if ( !element.attributes[ 'cke-dissolved' ] ) { // The symbol is usually the first text node descendant // of the element that doesn't start with a whitespace character; var symbol; element.forEach( function( element ) { // Sometimes there are custom markers represented as images. // They can be recognized by the distinctive alt attribute value. if ( !symbol && element.name == 'img' && element.attributes[ 'cke-ignored' ] && element.attributes.alt == '*' ) { symbol = '·'; // Remove the "symbol" now, since it's the best opportunity to do so. element.remove(); } }, CKEDITOR.NODE_ELEMENT ); element.forEach( function( element ) { if ( !symbol && !element.value.match( /^ / ) ) { symbol = element.value; } }, CKEDITOR.NODE_TEXT ); // Without a symbol this isn't really a list item. if ( typeof symbol == 'undefined' ) { return; } element.attributes[ 'cke-symbol' ] = symbol.replace( /(?: | ).*$/, '' ); List.removeSymbolText( element ); } var styles = element.attributes && tools.parseCssText( element.attributes.style ); // Default list has 40px padding. To correct indentation we need to reduce margin-left by 40px for each list level. // Additionally margin has to be reduced by sum of margins of each parent, however it can't be done until list are structured in a tree (#2870). // Note margin left is absent in IE pasted content. if ( styles[ 'margin-left' ] ) { var margin = styles[ 'margin-left' ], level = element.attributes[ 'cke-list-level' ]; // Ignore negative margins (#2870). margin = Math.max( CKEDITOR.tools.convertToPx( margin ) - 40 * level, 0 ); if ( margin ) { styles[ 'margin-left' ] = margin + 'px'; } else { delete styles[ 'margin-left' ]; } element.attributes.style = CKEDITOR.tools.writeCssText( styles ); } // Converting to a normal list item would implicitly wrap the element around an <ul>. element.name = 'cke:li'; }, /** * Converts any fake list items contained within `root` into real `<li>` elements. * * @private * @since 4.13.0 * @param {CKEDITOR.htmlParser.element} root * @returns {CKEDITOR.htmlParser.element[]} An array of converted elements. * @member CKEDITOR.plugins.pastetools.filters.word.lists */ convertToRealListItems: function( root ) { var listElements = []; // Select and clean up list elements. root.forEach( function( element ) { if ( element.name == 'cke:li' ) { element.name = 'li'; listElements.push( element ); } }, CKEDITOR.NODE_ELEMENT, false ); return listElements; }, removeSymbolText: function( element ) { // ...from a list element. var symbol = element.attributes[ 'cke-symbol' ], // Find the first element which contains symbol to be replaced (#2690). node = element.findOne( function( node ) { // Since symbol may contains special characters we use `indexOf` (instead of RegExp) which is sufficient (#877). return node.value && node.value.indexOf( symbol ) > -1; }, true ), parent; if ( node ) { node.value = node.value.replace( symbol, '' ); parent = node.parent; if ( parent.getHtml().match( /^(\s| )*$/ ) && parent !== element ) { parent.remove(); } else if ( !node.value ) { node.remove(); } } }, setListSymbol: function( list, symbol, level ) { level = level || 1; var style = tools.parseCssText( list.attributes.style ); if ( list.name == 'ol' ) { if ( list.attributes.type || style[ 'list-style-type' ] ) return; var typeMap = { '[ivx]': 'lower-roman', '[IVX]': 'upper-roman', '[a-z]': 'lower-alpha', '[A-Z]': 'upper-alpha', '\\d': 'decimal' }; for ( var type in typeMap ) { if ( List.getSubsectionSymbol( symbol ).match( new RegExp( type ) ) ) { style[ 'list-style-type' ] = typeMap[ type ]; break; } } list.attributes[ 'cke-list-style-type' ] = style[ 'list-style-type' ]; } else { var symbolMap = { '·': 'disc', 'o': 'circle', '§': 'square' // In Word this is a square. }; if ( !style[ 'list-style-type' ] && symbolMap[ symbol ] ) { style[ 'list-style-type' ] = symbolMap[ symbol ]; } } List.setListSymbol.removeRedundancies( style, level ); ( list.attributes.style = CKEDITOR.tools.writeCssText( style ) ) || delete list.attributes.style; }, setListStart: function( list ) { var symbols = [], offset = 0; for ( var i = 0; i < list.children.length; i++ ) { symbols.push( list.children[ i ].attributes[ 'cke-symbol' ] || '' ); } // When a list starts with a sublist, use the next element as a start indicator. if ( !symbols[ 0 ] ) { offset++; } // Attribute set in setListSymbol() switch ( list.attributes[ 'cke-list-style-type' ] ) { case 'lower-roman': case 'upper-roman': list.attributes.start = List.toArabic( List.getSubsectionSymbol( symbols[ offset ] ) ) - offset; break; case 'lower-alpha': case 'upper-alpha': list.attributes.start = List.getSubsectionSymbol( symbols[ offset ] ).replace( /\W/g, '' ).toLowerCase().charCodeAt( 0 ) - 96 - offset; break; case 'decimal': list.attributes.start = ( parseInt( List.getSubsectionSymbol( symbols[ offset ] ), 10 ) - offset ) || 1; break; } if ( list.attributes.start == '1' ) { delete list.attributes.start; } delete list.attributes[ 'cke-list-style-type' ]; }, /** * Numbering helper. * * @since 4.13.0 * @member CKEDITOR.plugins.pastetools.filters.word.lists */ numbering: { /** * Converts the list marker value into a decimal number. * * var toNumber = CKEDITOR.plugins.pastefromword.lists.numbering.toNumber; * * console.log( toNumber( 'XIV', 'upper-roman' ) ); // Logs 14. * console.log( toNumber( 'd', 'lower-alpha' ) ); // Logs 4. * console.log( toNumber( '35', 'decimal' ) ); // Logs 35. * console.log( toNumber( '404', 'foo' ) ); // Logs 1. * * @param {String} marker * @param {String} markerType Marker type according to CSS `list-style-type` values. * @returns {Number} * @member CKEDITOR.plugins.pastetools.filters.word.lists.numbering */ toNumber: function( marker, markerType ) { // Functions copied straight from old PFW implementation, no need to reinvent the wheel. function fromAlphabet( str ) { var alpahbets = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; str = str.toUpperCase(); var l = alpahbets.length, retVal = 1; for ( var x = 1; str.length > 0; x *= l ) { retVal += alpahbets.indexOf( str.charAt( str.length - 1 ) ) * x; str = str.substr( 0, str.length - 1 ); } return retVal; } function fromRoman( str ) { var 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' ] ]; str = str.toUpperCase(); var l = romans.length, retVal = 0; for ( var i = 0; i < l; ++i ) { for ( var j = romans[ i ], k = j[ 1 ].length; str.substr( 0, k ) == j[ 1 ]; str = str.substr( k ) ) retVal += j[ 0 ]; } return retVal; } if ( markerType == 'decimal' ) { return Number( marker ); } else if ( markerType == 'upper-roman' || markerType == 'lower-roman' ) { return fromRoman( marker.toUpperCase() ); } else if ( markerType == 'lower-alpha' || markerType == 'upper-alpha' ) { return fromAlphabet( marker ); } else { return 1; } }, /** * Returns a list style based on the Word marker content. * * var getStyle = CKEDITOR.plugins.pastefromword.lists.numbering.getStyle; * * console.log( getStyle( '4' ) ); // Logs: "decimal" * console.log( getStyle( 'b' ) ); // Logs: "lower-alpha" * console.log( getStyle( 'P' ) ); // Logs: "upper-alpha" * console.log( getStyle( 'i' ) ); // Logs: "lower-roman" * console.log( getStyle( 'X' ) ); // Logs: "upper-roman" * * * **Implementation note:** Characters `c` and `d` are not converted to roman on purpose. It is 100 and 500 respectively, so * you rarely go with a list up until this point, while it is common to start with `c` and `d` in alpha. * * @param {String} marker Marker content retained from Word, e.g. `1`, `7`, `XI`, `b`. * @returns {String} Resolved marker type. * @member CKEDITOR.plugins.pastetools.filters.word.lists.numbering */ getStyle: function( marker ) { var typeMap = { 'i': 'lower-roman', 'v': 'lower-roman', 'x': 'lower-roman', 'l': 'lower-roman', 'm': 'lower-roman', 'I': 'upper-roman', 'V': 'upper-roman', 'X': 'upper-roman', 'L': 'upper-roman', 'M': 'upper-roman' }, firstCharacter = marker.slice( 0, 1 ), type = typeMap[ firstCharacter ]; if ( !type ) { type = 'decimal'; if ( firstCharacter.match( /[a-z]/ ) ) { type = 'lower-alpha'; } if ( firstCharacter.match( /[A-Z]/ ) ) { type = 'upper-alpha'; } } return type; } }, // Taking into account cases like "1.1.2." etc. - get the last element. getSubsectionSymbol: function( symbol ) { return ( symbol.match( /([\da-zA-Z]+).?$/ ) || [ 'placeholder', '1' ] )[ 1 ]; }, setListDir: function( list ) { var dirs = { ltr: 0, rtl: 0 }; list.forEach( function( child ) { if ( child.name == 'li' ) { var dir = child.attributes.dir || child.attributes.DIR || ''; if ( dir.toLowerCase() == 'rtl' ) { dirs.rtl++; } else { dirs.ltr++; } } }, CKEDITOR.ELEMENT_NODE ); if ( dirs.rtl > dirs.ltr ) { list.attributes.dir = 'rtl'; } }, createList: function( element ) { // "o" symbolizes a circle in unordered lists. if ( ( element.attributes[ 'cke-symbol' ].match( /([\da-np-zA-NP-Z]).?/ ) || [] )[ 1 ] ) { return new CKEDITOR.htmlParser.element( 'ol' ); } return new CKEDITOR.htmlParser.element( 'ul' ); }, /** * @private * @since 4.13.0 * @param {CKEDITOR.htmlParser.element} root An element to be looked through for lists. * @returns {CKEDITOR.htmlParser.element[]} An array of created list items. * @member CKEDITOR.plugins.pastetools.filters.word.lists */ createLists: function( root ) { var element, level, i, j, listElements = List.convertToRealListItems( root ); if ( listElements.length === 0 ) { return []; } // Chop data into continuous lists. var lists = List.groupLists( listElements ); // Create nested list structures. for ( i = 0; i < lists.length; i++ ) { var list = lists[ i ], firstLevel1Element = list[ 0 ]; // To determine the type of the top-level list a level 1 element is needed. for ( j = 0; j < list.length; j++ ) { if ( list[ j ].attributes[ 'cke-list-level' ] == 1 ) { firstLevel1Element = list[ j ]; break; } } var containerStack = [ List.createList( firstLevel1Element ) ], // List wrapper (ol/ul). innermostContainer = containerStack[ 0 ], allContainers = [ containerStack[ 0 ] ]; // Insert first known list item before the list wrapper. innermostContainer.insertBefore( list[ 0 ] ); for ( j = 0; j < list.length; j++ ) { element = list[ j ]; level = element.attributes[ 'cke-list-level' ]; while ( level > containerStack.length ) { var content = List.createList( element ); var children = innermostContainer.children; if ( children.length > 0 ) { children[ children.length - 1 ].add( content ); } else { var container = new CKEDITOR.htmlParser.element( 'li', { style: 'list-style-type:none' } ); container.add( content ); innermostContainer.add( container ); } containerStack.push( content ); allContainers.push( content ); innermostContainer = content; if ( level == containerStack.length ) { List.setListSymbol( content, element.attributes[ 'cke-symbol' ], level ); } } while ( level < containerStack.length ) { containerStack.pop(); innermostContainer = containerStack[ containerStack.length - 1 ]; if ( level == containerStack.length ) { List.setListSymbol( innermostContainer, element.attributes[ 'cke-symbol' ], level ); } } // For future reference this is where the list elements are actually put into the lists. element.remove(); innermostContainer.add( element ); } // Try to set the symbol for the root (level 1) list. var level1Symbol; if ( containerStack[ 0 ].children.length ) { level1Symbol = containerStack[ 0 ].children[ 0 ].attributes[ 'cke-symbol' ]; if ( !level1Symbol && containerStack[ 0 ].children.length > 1 ) { level1Symbol = containerStack[0].children[1].attributes[ 'cke-symbol' ]; } if ( level1Symbol ) { List.setListSymbol( containerStack[ 0 ], level1Symbol ); } } // This can be done only after all the list elements are where they should be. for ( j = 0; j < allContainers.length; j++ ) { List.setListStart( allContainers[ j ] ); } // Last but not least apply li[start] if needed, also this needs to be done once ols are final. for ( j = 0; j < list.length; j++ ) { this.determineListItemValue( list[ j ] ); } } // Adjust left margin based on parents sum of parents left margin (#2870). CKEDITOR.tools.array.forEach( listElements, function( element ) { var listParents = getParentListItems( element ), leftOffset = getTotalMarginLeft( listParents ), styles, marginLeft; if ( !leftOffset ) { return; } element.attributes = element.attributes || {}; styles = CKEDITOR.tools.parseCssText( element.attributes.style ); marginLeft = styles[ 'margin-left' ] || 0; marginLeft = Math.max( parseInt( marginLeft, 10 ) - leftOffset, 0 ); if ( marginLeft ) { styles[ 'margin-left' ] = marginLeft + 'px'; } else { delete styles[ 'margin-left' ]; } element.attributes.style = CKEDITOR.tools.writeCssText( styles ); } ); return listElements; function getParentListItems( element ) { var parents = [], parent = element.parent; while ( parent ) { if ( parent.name === 'li' ) { parents.push( parent ); } parent = parent.parent; } return parents; } function getTotalMarginLeft( elements ) { return CKEDITOR.tools.array.reduce( elements, function( total, element ) { if ( element.attributes && element.attributes.style ) { var marginLeft = CKEDITOR.tools.parseCssText( element.attributes.style )[ 'margin-left' ]; } return marginLeft ? total + parseInt( marginLeft, 10 ) : total; }, 0 ); } }, /** * Final cleanup — removes all `cke-*` helper attributes. * * @private * @since 4.13.0 * @param {CKEDITOR.htmlParser.element[]} listElements * @member CKEDITOR.plugins.pastetools.filters.word.lists */ cleanup: function( listElements ) { var tempAttributes = [ 'cke-list-level', 'cke-symbol', 'cke-list-id', 'cke-indentation', 'cke-dissolved' ], i, j; for ( i = 0; i < listElements.length; i++ ) { for ( j = 0; j < tempAttributes.length; j++ ) { delete listElements[ i ].attributes[ tempAttributes[ j ] ]; } } }, /** * Tries to determine the `li[value]` attribute for a given list item. The `element` given must * have a parent in order for this function to work properly. * * @private * @since 4.13.0 * @param {CKEDITOR.htmlParser.element} element * @member CKEDITOR.plugins.pastetools.filters.word.lists */ determineListItemValue: function( element ) { if ( element.parent.name !== 'ol' ) { // li[value] make sense only for list items in ordered list. return; } var assumedValue = this.calculateValue( element ), cleanSymbol = element.attributes[ 'cke-symbol' ].match( /[a-z0-9]+/gi ), computedValue, listType; if ( cleanSymbol ) { // Note that we always want to use last match, just because of markers like "1.1.4" "1.A.a.IV" etc. cleanSymbol = cleanSymbol[ cleanSymbol.length - 1 ]; // We can determine proper value only if we know what type of list is it. // So we need to check list wrapper if it has this information. listType = element.parent.attributes[ 'cke-list-style-type' ] || this.numbering.getStyle( cleanSymbol ); computedValue = this.numbering.toNumber( cleanSymbol, listType ); if ( computedValue !== assumedValue ) { element.attributes.value = computedValue; } } }, /** * Calculates the value for a given `<li>` element based on preceding list items (e.g. the `value` * attribute). It could also look at the start attribute of its parent list (`<ol>`). * * @private * @since 4.13.0 * @param {CKEDITOR.htmlParser.element} element The `<li>` element. * @returns {Number} * @member CKEDITOR.plugins.pastetools.filters.word.lists */ calculateValue: function( element ) { if ( !element.parent ) { return 1; } var list = element.parent, elementIndex = element.getIndex(), valueFound = null, // Index of the element with value attribute. valueElementIndex, curElement, i; // Look for any preceding li[value]. for ( i = elementIndex; i >= 0 && valueFound === null; i-- ) { curElement = list.children[ i ]; if ( curElement.attributes && curElement.attributes.value !== undefined ) { valueElementIndex = i; valueFound = parseInt( curElement.attributes.value, 10 ); } } // Still if no li[value] was found, we'll check the list. if ( valueFound === null ) { valueFound = list.attributes.start !== undefined ? parseInt( list.attributes.start, 10 ) : 1; valueElementIndex = 0; } return valueFound + ( elementIndex - valueElementIndex ); }, /** * @private * @since 4.13.0 * @param {CKEDITOR.htmlParser.element} element * @member CKEDITOR.plugins.pastetools.filters.word.lists */ dissolveList: function( element ) { var nameIs = function( name ) { return function( element ) { return element.name == name; }; }, isList = function( element ) { return nameIs( 'ul' )( element ) || nameIs( 'ol' )( element ); }, arrayTools = CKEDITOR.tools.array, elements = [], children, i; element.forEach( function( child ) { elements.push( child ); }, CKEDITOR.NODE_ELEMENT, false ); var items = arrayTools.filter( elements, nameIs( 'li' ) ), lists = arrayTools.filter( elements, isList ); arrayTools.forEach( lists, function( list ) { var type = list.attributes.type, start = parseInt( list.attributes.start, 10 ) || 1, level = countParents( isList, list ) + 1; if ( !type ) { var style = tools.parseCssText( list.attributes.style ); type = style[ 'list-style-type' ]; } arrayTools.forEach( arrayTools.filter( list.children, nameIs( 'li' ) ), function( child, index ) { var symbol; switch ( type ) { case 'disc': symbol = '·'; break; case 'circle': symbol = 'o'; break; case 'square': symbol = '§'; break; case '1': case 'decimal': symbol = ( start + index ) + '.'; break; case 'a': case 'lower-alpha': symbol = String.fromCharCode( 'a'.charCodeAt( 0 ) + start - 1 + index ) + '.'; break; case 'A': case 'upper-alpha': symbol = String.fromCharCode( 'A'.charCodeAt( 0 ) + start - 1 + index ) + '.'; break; case 'i': case 'lower-roman': symbol = toRoman( start + index ) + '.'; break; case 'I': case 'upper-roman': symbol = toRoman( start + index ).toUpperCase() + '.'; break; default: symbol = list.name == 'ul' ? '·' : ( start + index ) + '.'; } child.attributes[ 'cke-symbol' ] = symbol; child.attributes[ 'cke-list-level' ] = level; } ); } ); children = arrayTools.reduce( items, function( acc, listElement ) { var child = listElement.children[ 0 ]; if ( child && child.name && child.attributes.style && child.attributes.style.match( /mso-list:/i ) ) { Style.pushStylesLower( listElement, { 'list-style-type': true, 'display': true } ); var childStyle = tools.parseCssText( child.attributes.style, true ); Style.setStyle( listElement, 'mso-list', childStyle[ 'mso-list' ], true ); Style.setStyle( child, 'mso-list', '' ); // mso-list takes precedence in determining the level. delete listElement[ 'cke-list-level' ]; // If this style has a value it's usually "none". This marks such list elements for deletion. var styleName = childStyle.display ? 'display' : childStyle.DISPLAY ? 'DISPLAY' : ''; if ( styleName ) { Style.setStyle( listElement, 'display', childStyle[ styleName ], true ); } } // Don't include elements put there only to contain another list. if ( listElement.children.length === 1 && isList( listElement.children[ 0 ] ) ) { return acc; } listElement.name = 'p'; listElement.attributes[ 'cke-dissolved' ] = true; acc.push( listElement ); return acc; }, [] ); for ( i = children.length - 1; i >= 0; i-- ) { children[ i ].insertAfter( element ); } for ( i = lists.length - 1; i >= 0; i-- ) { delete lists[ i ].name; } function toRoman( number ) { if ( number >= 50 ) return 'l' + toRoman( number - 50 ); if ( number >= 40 ) return 'xl' + toRoman( number - 40 ); if ( number >= 10 ) return 'x' + toRoman( number - 10 ); if ( number == 9 ) return 'ix'; if ( number >= 5 ) return 'v' + toRoman( number - 5 ); if ( number == 4 ) return 'iv'; if ( number >= 1 ) return 'i' + toRoman( number - 1 ); return ''; } function countParents( condition, element ) { return count( element, 0 ); function count( parent, number ) { if ( !parent || !parent.parent ) { return number; } if ( condition( parent.parent ) ) { return count( parent.parent, number + 1 ); } else { return count( parent.parent, number ); } } } }, groupLists: function( listElements ) { // Chop data into continuous lists. var i, element, lists = [ [ listElements[ 0 ] ] ], lastList = lists[ 0 ]; element = listElements[ 0 ]; element.attributes[ 'cke-indentation' ] = element.attributes[ 'cke-indentation' ] || getElementIndentation( element ); for ( i = 1; i < listElements.length; i++ ) { element = listElements[ i ]; var previous = listElements[ i - 1 ]; element.attributes[ 'cke-indentation' ] = element.attributes[ 'cke-indentation' ] || getElementIndentation( element ); if ( element.previous !== previous ) { List.chopDiscontinuousLists( lastList, lists ); lists.push( lastList = [] ); } lastList.push( element ); } List.chopDiscontinuousLists( lastList, lists ); return lists; }, /** * Converts a single, flat list items array into an array with a hierarchy of items. * * As the list gets chopped, it will be forced to render as a separate list, even if it has a deeper nesting level. * For example, for level 3 it will create a structure like `ol > li > ol > li > ol > li`. * * Note that list items within a single list but with different levels that did not get chopped * will still be rendered as a list tree later. * * @private * @since 4.13.0 * @param {CKEDITOR.htmlParser.element[]} list An array containing list items. * @param {CKEDITOR.htmlParser.element[]} lists All the lists in the pasted content represented by an array of arrays * of list items. Modified by this method. * @member CKEDITOR.plugins.pastetools.filters.word.lists */ chopDiscontinuousLists: function( list, lists ) { var levelSymbols = {}; var choppedLists = [ [] ], lastListInfo; for ( var i = 0; i < list.length; i++ ) { var lastSymbol = levelSymbols[ list[ i ].attributes[ 'cke-list-level' ] ], currentListInfo = this.getListItemInfo( list[ i ] ), currentSymbol, forceType; if ( lastSymbol ) { // An "h" before an "i". forceType = lastSymbol.type.match( /alpha/ ) && lastSymbol.index == 7 ? 'alpha' : forceType; // An "n" before an "o". forceType = list[ i ].attributes[ 'cke-symbol' ] == 'o' && lastSymbol.index == 14 ? 'alpha' : forceType; currentSymbol = List.getSymbolInfo( list[ i ].attributes[ 'cke-symbol' ], forceType ); currentListInfo = this.getListItemInfo( list[ i ] ); // Based on current and last index we'll decide if we want to chop list. if ( // If the last list was a different list type then chop it! lastSymbol.type != currentSymbol.type || // If those are logically different lists, and current list is not a continuation (https://dev.ckeditor.com/ticket/7918): ( lastListInfo && currentListInfo.id != lastListInfo.id && !this.isAListContinuation( list[ i ] ) ) ) { choppedLists.push( [] ); } } else { currentSymbol = List.getSymbolInfo( list[ i ].attributes[ 'cke-symbol' ] ); } // Reset all higher levels for ( var j = parseInt( list[ i ].attributes[ 'cke-list-level' ], 10 ) + 1; j < 20; j++ ) { if ( levelSymbols[ j ] ) { delete levelSymbols[ j ]; } } levelSymbols[ list[ i ].attributes[ 'cke-list-level' ] ] = currentSymbol; choppedLists[ choppedLists.length - 1 ].push( list[ i ] ); lastListInfo = currentListInfo; } [].splice.apply( lists, [].concat( [ tools.indexOf( lists, list ), 1 ], choppedLists ) ); }, /** * Checks if this list is a direct continuation of a list interrupted by a list with a different ID and * with a different level. So if you look at the following list: * * * list1 level1 * * list1 level1 * * list2 level2 * * list2 level2 * * list1 level1 * * It would return `true`, which means it is a continuation, and should not be chopped. However, if any paragraph or * anything else appears in-between, it should be broken into different lists. * * You can see fixtures from issue https://dev.ckeditor.com/ticket/7918 as an example. * * @private * @since 4.13.0 * @param {CKEDITOR.htmlParser.element} listElement The list to be checked. * @returns {Boolean} * @member CKEDITOR.plugins.pastetools.filters.word.lists */ isAListContinuation: function( listElement ) { var prev = listElement; do { prev = prev.previous; if ( prev && prev.type === CKEDITOR.NODE_ELEMENT ) { if ( prev.attributes[ 'cke-list-level' ] === undefined ) { // Not a list, so looks like an interrupted list. return false; } if ( prev.attributes[ 'cke-list-level' ] === listElement.attributes[ 'cke-list-level' ] ) { // Same level, so we want to check if this is a continuation. return prev.attributes[ 'cke-list-id' ] === listElement.attributes[ 'cke-list-id' ]; } } } while ( prev ); return false; }, // Source: http://stackoverflow.com/a/17534350/3698944 toArabic: function( symbol ) { if ( !symbol.match( /[ivxl]/i ) ) return 0; if ( symbol.match( /^l/i ) ) return 50 + List.toArabic( symbol.slice( 1 ) ); if ( symbol.match( /^lx/i ) ) return 40 + List.toArabic( symbol.slice( 1 ) ); if ( symbol.match( /^x/i ) ) return 10 + List.toArabic( symbol.slice( 1 ) ); if ( symbol.match( /^ix/i ) ) return 9 + List.toArabic( symbol.slice( 2 ) ); if ( symbol.match( /^v/i ) ) return 5 + List.toArabic( symbol.slice( 1 ) ); if ( symbol.match( /^iv/i ) ) return 4 + List.toArabic( symbol.slice( 2 ) ); if ( symbol.match( /^i/i ) ) return 1 + List.toArabic( symbol.slice( 1 ) ); // Ignore other characters. return List.toArabic( symbol.slice( 1 ) ); }, /** * Returns an object describing the given `symbol`. * * @private * @since 4.13.0 * @param {String} symbol * @param {String} type * @returns {Object} ret * @returns {Number} ret.index Identified numbering value * @returns {String} ret.type One of: `decimal`, `disc`, `circle`, `square`, `roman`, `alpha`. * @member CKEDITOR.plugins.pastetools.filters.word.lists */ getSymbolInfo: function( symbol, type ) { var symbolCase = symbol.toUpperCase() == symbol ? 'upper-' : 'lower-', symbolMap = { '·': [ 'disc', -1 ], 'o': [ 'circle', -2 ], '§': [ 'square', -3 ] }; if ( symbol in symbolMap || ( type && type.match( /(disc|circle|square)/ ) ) ) { return { index: symbolMap[ symbol ][ 1 ], type: symbolMap[ symbol ][ 0 ] }; } if ( symbol.match( /\d/ ) ) { return { index: symbol ? parseInt( List.getSubsectionSymbol( symbol ) , 10 ) : 0, type: 'decimal' }; } symbol = symbol.replace( /\W/g, '' ).toLowerCase(); if ( ( !type && symbol.match( /[ivxl]+/i ) ) || ( type && type != 'alpha' ) || type == 'roman' ) { return { index: List.toArabic( symbol ), type: symbolCase + 'roman' }; } if ( symbol.match( /[a-z]/i ) ) { return { index: symbol.charCodeAt( 0 ) - 97, type: symbolCase + 'alpha' }; } return { index: -1, type: 'disc' }; }, /** * Returns Word-generated information about the given list item, mainly by parsing the `mso-list` * CSS property. * * Note: Paragraphs with `mso-list` are also counted as list items because Word serves * list items as paragraphs. * * @private * @since 4.13.0 * @param {CKEDITOR.htmlParser.element} list * @returns ret * @returns {String} ret.id List ID. Usually it is a decimal string. * @returns {String} ret.level List nesting level. `0` means it is the outermost list. Usually it is * a decimal string. * @member CKEDITOR.plugins.pastetools.filters.word.lists */ getListItemInfo: function( list ) { if ( list.attributes[ 'cke-list-id' ] !== undefined ) { // List was already resolved. return { id: list.attributes[ 'cke-list-id' ], level: list.attributes[ 'cke-list-level' ] }; } var propValue = tools.parseCssText( list.attributes.style )[ 'mso-list' ], ret = { id: '0', level: '1' }; if ( propValue ) { // Add one whitespace so it's easier to match values assuming that all of these are separated with \s. propValue += ' '; ret.level = propValue.match( /level(.+?)\s+/ )[ 1 ]; ret.id = propValue.match( /l(\d+?)\s+/ )[ 1 ]; } // Store values. List level will be reused if present to prevent regressions. list.attributes[ 'cke-list-level' ] = list.attributes[ 'cke-list-level' ] !== undefined ? list.attributes[ 'cke-list-level' ] : ret.level; list.attributes[ 'cke-list-id' ] = ret.id; return ret; } }; List = plug.lists; /** * Namespace containing a set of image helper methods. * * @private * @since 4.13.0 * @member CKEDITOR.plugins.pastetools.filters.word */ plug.images = { /** * Parses RTF content to find embedded images. Please be aware that this method should only return `png` and `jpeg` images. * * @private * @since 4.13.0 * @param {String} rtfContent RTF content to be checked for images. * @returns {Object[]} An array of images found in the `rtfContent`. * @returns {String} return.hex Hexadecimal string of an image embedded in `rtfContent`. * @returns {String} return.type A string representing the image type. Allowed values: 'image/png', 'image/jpeg'. * @member CKEDITOR.plugins.pastetools.filters.word.images */ extractFromRtf: function( rtfContent ) { var ret = [], rePictureHeader = /\{\\pict[\s\S]+?\\bliptag\-?\d+(\\blipupi\-?\d+)?(\{\\\*\\blipuid\s?[\da-fA-F]+)?[\s\}]*?/, rePicture = new RegExp( '(?:(' + rePictureHeader.source + '))([\\da-fA-F\\s]+)\\}', 'g' ), wholeImages, imageType; wholeImages = rtfContent.match( rePicture ); if ( !wholeImages ) { return ret; } for ( var i = 0; i < wholeImages.length; i++ ) { if ( rePictureHeader.test( wholeImages[ i ] ) ) { if ( wholeImages[ i ].indexOf( '\\pngblip' ) !== -1 ) { imageType = 'image/png'; } else if ( wholeImages[ i ].indexOf( '\\jpegblip' ) !== -1 ) { imageType = 'image/jpeg'; } else { continue; } ret.push( { hex: imageType ? wholeImages[ i ].replace( rePictureHeader, '' ).replace( /[^\da-fA-F]/g, '' ) : null, type: imageType } ); } } return ret; }, /** * Extracts an array of `src`` attributes in `<img>` tags from the given HTML. `<img>` tags belonging to VML shapes are removed. * * CKEDITOR.plugins.pastefromword.images.extractTagsFromHtml( html ); * // Returns: [ 'http://example-picture.com/random.png', 'http://example-picture.com/another.png' ] * * @private * @since 4.13.0 * @param {String} html A string representing HTML code. * @returns {String[]} An array of strings representing the `src` attribute of the `<img>` tags found in `html`. * @member CKEDITOR.plugins.pastetools.filters.word.images */ extractTagsFromHtml: function( html ) { var regexp = /<img[^>]+src="([^"]+)[^>]+/g, ret = [], item; while ( item = regexp.exec( html ) ) { ret.push( item[ 1 ] ); } return ret; } }; /** * Namespace containing methods used to process the pasted content using heuristics. * * @private * @since 4.13.0 * @member CKEDITOR.plugins.pastetools.filters.word */ plug.heuristics = { /** * Decides if an `item` looks like a list item in Microsoft Edge. * * Note: It will return `false` when run in a browser other than Microsoft Edge, despite the configuration. * * @param {CKEDITOR.editor} editor * @param {CKEDITOR.htmlParser.element} item * @returns {Boolean} * @member CKEDITOR.plugins.pastetools.filters.word.heuristics * @private */ isEdgeListItem: function( editor, item ) { if ( !CKEDITOR.env.edge || !editor.config.pasteFromWord_heuristicsEdgeList ) { return false; } var innerText = ''; // Edge doesn't provide any list-specific markup, so the only way to guess if it's a list is to check the text structure. item.forEach && item.forEach( function( text ) { innerText += text.value; }, CKEDITOR.NODE_TEXT ); if ( innerText.match( /^(?: | )*\(?[a-zA-Z0-9]+?[\.\)](?: | ){2,}/ ) ) { return true; } return Heuristics.isDegenerateListItem( editor, item ); }, /** * Cleans up a given list `item`. It is needed to remove Edge pre-marker indentation, since Edge pastes * list items as plain paragraphs with multiple ` `s before the list marker. * * @since 4.7.0 * @param {CKEDITOR.htmlParser.element} item The pre-processed list-like item, like a paragraph. * @member CKEDITOR.plugins.pastetools.filters.word.heuristics * @private */ cleanupEdgeListItem: function( item ) { var textOccurred = false; item.forEach( function( node ) { if ( !textOccurred ) { node.value = node.value.replace( /^(?: |[\s])+/, '' ); // If there's any remaining text beside nbsp it means that we can stop filtering. if ( node.value.length ) { textOccurred = true; } } }, CKEDITOR.NODE_TEXT ); }, /** * Checks whether an element is a degenerate list item. * * Degenerate list items are elements that have some styles specific to list items, * but lack the ones that could be used to determine their features (like list level etc.). * * @param {CKEDITOR.editor} editor * @param {CKEDITOR.htmlParser.element} item * @returns {Boolean} * @member CKEDITOR.plugins.pastetools.filters.word.heuristics * @private * */ isDegenerateListItem: function( editor, item ) { return !!item.attributes[ 'cke-list-level' ] || ( item.attributes.style && !item.attributes.style.match( /mso\-list/ ) && !!item.find( function( child ) { // In rare cases there's no indication that a heading is a list item other than // the fact that it has a child element containing only a list symbol. if ( child.type == CKEDITOR.NODE_ELEMENT && item.name.match( /h\d/i ) && child.getHtml().match( /^[a-zA-Z0-9]+?[\.\)]$/ ) ) { return true; } var css = tools.parseCssText( child.attributes && child.attributes.style, true ); if ( !css ) { return false; } var fontSize = css.font || css['font-size'] || '', fontFamily = css[ 'font-family' ] || ''; return ( fontSize.match( /7pt/i ) && !!child.previous ) || fontFamily.match( /symbol/i ); }, true ).length ); }, /** * Assigns list levels to the `item` and all directly subsequent nodes for which {@link #isEdgeListItem} returns `true`. * * The algorithm determines list item level based on the lowest common non-zero difference in indentation * of two or more subsequent list-like elements. * * @param {CKEDITOR.editor} editor * @param {CKEDITOR.htmlParser.element} item The first item of the list. * @returns {Object/null} `null` if list levels were already applied, or an object used to verify results in tests. * @returns {Number[]} return.indents * @returns {Number[]} return.levels * @returns {Number[]} return.diffs * @member CKEDITOR.plugins.pastetools.filters.word.heuristics * @private */ assignListLevels: function( editor, item ) { // If levels were already calculated, it means that this function was called for preceeding element. There's // no need to do this heavy work. if ( item.attributes && item.attributes[ 'cke-list-level' ] !== undefined ) { return; } var indents = [ getElementIndentation( item ) ], items = [ item ], levels = [], array = CKEDITOR.tools.array, map = array.map; while ( item.next && item.next.attributes && !item.next.attributes[ 'cke-list-level' ] && Heuristics.isDegenerateListItem( editor, item.next ) ) { item = item.next; indents.push( getElementIndentation( item ) ); items.push( item ); } // An array with indentation difference between n and n-1 list item. It's 0 for the first one. var indentationDiffs = map( indents, function( curIndent, i ) { return i === 0 ? 0 : curIndent - indents[ i - 1 ]; } ), // Guess indentation step, but it must not be equal to 0. indentationPerLevel = this.guessIndentationStep( array.filter( indents, function( val ) { return val !== 0; } ) ); // Here's the tricky part, we need to magically figure out what is the indentation difference between list level. levels = map( indents, function( val ) { // Make sure that the level is a full number. return Math.round( val / indentationPerLevel ); } ); // Level can not be equal to 0, in case if it happens bump all the levels by 1, if ( array.indexOf( levels, 0 ) !== -1 ) { levels = map( levels, function( val ) { return val + 1; } ); } // Assign levels to a proper place. array.forEach( items, function( curItem, index ) { curItem.attributes[ 'cke-list-level' ] = levels[ index ]; } ); return { indents: indents, levels: levels, diffs: indentationDiffs }; }, /** * Given an array of list indentations, this method tries to guess what the indentation difference per list level is. * E.g. assuming that you have something like: * * * foo (indentation 30px) * * bar (indentation 90px) * * baz (indentation 90px) * * baz (indentation 115px) * * baz (indentation 60px) * * The method will return `30`. * * @param {Number[]} indentations An array of indentation sizes. * @returns {Number/null} A number or `null` if empty `indentations` was given. * @member CKEDITOR.plugins.pastetools.filters.word.heuristics * @private */ guessIndentationStep: function( indentations ) { return indentations.length ? Math.min.apply( null, indentations ) : null; }, /** * Shifts lists that were deformed during pasting one level down * so that the list structure matches the content copied from Word. * * @param {CKEDITOR.htmlParser.element} element * @member CKEDITOR.plugins.pastetools.filters.word.heuristics * @private * */ correctLevelShift: function( element ) { var isShiftedList = function( list ) { return list.children && list.children.length == 1 && Heuristics.isShifted( list.children[ 0 ] ); }; if ( this.isShifted( element ) ) { var lists = CKEDITOR.tools.array.filter( element.children, function( child ) { return ( child.name == 'ul' || child.name == 'ol' ); } ); var listChildren = CKEDITOR.tools.array.reduce( lists, function( acc, list ) { var preceding = isShiftedList( list ) ? [ list ] : list.children; return preceding.concat( acc ); }, [] ); CKEDITOR.tools.array.forEach( lists, function( list ) { list.remove(); } ); CKEDITOR.tools.array.forEach( listChildren, function( child ) { // `Add` method without index always append child at the end (#796). element.add( child ); } ); delete element.name; } }, /** * Determines if the list is malformed in a manner that its items * are one level deeper than they should be. * * @param {CKEDITOR.htmlParser.element} element * @returns {Boolean} * @member CKEDITOR.plugins.pastetools.filters.word.heuristics * @private */ isShifted: function( element ) { if ( element.name !== 'li' ) { return false; } return CKEDITOR.tools.array.filter( element.children, function( child ) { if ( child.name ) { if ( child.name == 'ul' || child.name == 'ol' ) { return false; } if ( child.name == 'p' && child.children.length === 0 ) { return false; } } return true; } ).length === 0; } }; Heuristics = plug.heuristics; // Expose this function since it's useful in other places. List.setListSymbol.removeRedundancies = function( style, level ) { // 'disc' and 'decimal' are the default styles in some cases - remove redundancy. if ( ( level === 1 && style[ 'list-style-type' ] === 'disc' ) || style[ 'list-style-type' ] === 'decimal' ) { delete style[ 'list-style-type' ]; } }; function falseIfEmpty( value ) { if ( value === '' ) { return false; } return value; } // Used when filtering attributes - returning false deletes the attribute. function remove() { return false; } CKEDITOR.cleanWord = CKEDITOR.pasteFilters.word = pastetools.createFilter( { rules: [ commonFilter.rules, plug.rules ], additionalTransforms: function( html ) { // Before filtering inline all the styles to allow because some of them are available only in style // sheets. This step is skipped in IEs due to their flaky support for custom types in dataTransfer. (https://dev.ckeditor.com/ticket/16847) if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ) { html = commonFilter.styles.inliner.inline( html ).getBody().getHtml(); } // Sometimes Word malforms the comments. return html.replace( /<!\[/g, '<!--[' ).replace( /\]>/g, ']-->' ); } } ); /** * See {@link CKEDITOR.plugins.pastetools.filters.word.lists}. * * @property {Object} lists * @private * @deprecated 4.13.0 * @since 4.6.0 * @member CKEDITOR.plugins.pastefromword */ /** * See {@link CKEDITOR.plugins.pastetools.filters.word.images}. * * @property {Object} images * @private * @deprecated 4.13.0 * @since 4.8.0 * @member CKEDITOR.plugins.pastefromword */ /** * See {@link CKEDITOR.plugins.pastetools.filters.word.heuristics}. * * @property {Object} heuristics * @private * @deprecated 4.13.0 * @since 4.6.2 * @member CKEDITOR.plugins.pastefromword */ /** * See {@link CKEDITOR.plugins.pastetools.filters.common.styles}. * * @property {Object} styles * @private * @deprecated 4.13.0 * @since 4.6.0 * @member CKEDITOR.plugins.pastefromword */ /** * See {@link #pasteTools_removeFontStyles}. * * **Important note:** Prior to version 4.6.0 this configuration option defaulted to `true`. * * @deprecated 4.13.0 * @since 3.1.0 * @cfg {Boolean} [pasteFromWordRemoveFontStyles=false] * @member CKEDITOR.config */ /** * Whether to transform Microsoft Word outline numbered headings into lists. * * config.pasteFromWordNumberedHeadingToList = true; * * @removed 4.6.0 * @since 3.1.0 * @cfg {Boolean} [pasteFromWordNumberedHeadingToList=false] * @member CKEDITOR.config */ /** * Whether to remove element styles that cannot be managed with the editor. Note * that this option does not handle font-specific styles, which depend on the * {@link #pasteTools_removeFontStyles} setting instead. * * config.pasteFromWordRemoveStyles = false; * * @removed 4.6.0 * @since 3.1.0 * @cfg {Boolean} [pasteFromWordRemoveStyles=true] * @member CKEDITOR.config */ /** * Activates a heuristic that helps detect lists pasted into the editor in Microsoft Edge. * * The reason why this heuristic is needed is that on pasting Microsoft Edge removes any Word-specific * metadata allowing to identify lists. * * // Disables list heuristics for Edge. * config.pasteFromWord_heuristicsEdgeList = false; * * @since 4.6.2 * @cfg {Boolean} [pasteFromWord_heuristicsEdgeList=true] * @member CKEDITOR.config */ CKEDITOR.config.pasteFromWord_heuristicsEdgeList = true; } )();