UNPKG

es-ckeditor

Version:

CKEditor-based implementation and add some plugins, For example kityformula etc.

1,406 lines (1,187 loc) 68.8 kB
/** * @license Copyright (c) 2003-2021, 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 = 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|&nbsp;)+$/ ) ) { 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( /&nbsp;/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( /&nbsp;/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 ); } } function every(array, fn, thisArg) { // Empty arrays always return true. if ( !array.length ) { return true; } var ret = filter( array, fn, thisArg ); return array.length === ret.length; } function filter(array, fn, thisArg) { var ret = []; forEach( array, function( val, i ) { if ( fn.call( thisArg, val, i, array ) ) { ret.push( val ); } } ); return ret; } function forEach(array, fn, thisArg) { var len = array.length, i; for ( i = 0; i < len; i++ ) { fn.call( thisArg, array[ i ], i, array ); } } }; /** * 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( /(?: |&nbsp;).*$/, '' ); 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|&nbsp;)*$/ ) && 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 &mdash; 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 is