es-ckeditor
Version:
CKEditor-based implementation and add some plugins, For example kityformula etc.
1,406 lines (1,187 loc) • 68.8 kB
JavaScript
/**
* @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| )+$/ ) ) {
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 );
}
}
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( /(?: | ).*$/, '' );
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 is