UNPKG

es-ckeditor

Version:

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

1,397 lines (1,167 loc) 44.6 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 Style, tools = CKEDITOR.tools, plug = {}; /** * A set of common paste filter helpers. * * @since 4.13.0 * @private * @member CKEDITOR.plugins.pastetools.filters */ CKEDITOR.plugins.pastetools.filters.common = plug; /** * Common paste rules. * * @since 4.13.0 * @private * @member CKEDITOR.plugins.pastetools.filters.common */ plug.rules = function( html, editor, filter ) { var availableFonts = getMatchingFonts( editor ); return { elements: { '^': function( element ) { removeSuperfluousStyles( element ); // Don't use "attributeNames", because those rules are applied after elements. // Normalization is required at the very begininng. normalizeAttributesName( element ); }, 'span': function( element ) { if ( element.hasClass( 'Apple-converted-space' ) ) { return new CKEDITOR.htmlParser.text( ' ' ); } }, 'table': function( element ) { element.filterChildren( filter ); var parent = element.parent, root = parent && parent.parent, parentChildren, i; // In case parent div has only align attr, move it to the table element (https://dev.ckeditor.com/ticket/16811). if ( parent.name && parent.name === 'div' && parent.attributes.align && Object.keys( parent.attributes ).length === 1 && parent.children.length === 1 ) { // If align is the only attribute of parent. element.attributes.align = parent.attributes.align; parentChildren = parent.children.splice( 0 ); element.remove(); for ( i = parentChildren.length - 1; i >= 0; i-- ) { root.add( parentChildren[ i ], parent.getIndex() ); } parent.remove(); } Style.convertStyleToPx( element ); }, 'tr': function( element ) { // Attribues are moved to 'td' elements. element.attributes = {}; }, 'td': function( element ) { var ascendant = element.getAscendant( 'table' ), ascendantStyle = tools.parseCssText( ascendant.attributes.style, true ); // Sometimes the background is set for the whole table - move it to individual cells. var background = ascendantStyle.background; if ( background ) { Style.setStyle( element, 'background', background, true ); } var backgroundColor = ascendantStyle[ 'background-color' ]; if ( backgroundColor ) { Style.setStyle( element, 'background-color', backgroundColor, true ); } var styles = tools.parseCssText( element.attributes.style, true ), borderStyles = styles.border ? CKEDITOR.tools.style.border.fromCssRule( styles.border ) : {}, borders = tools.style.border && tools.style.border.splitCssValues( styles, borderStyles ), tmpStyles = CKEDITOR.tools.clone( styles ); // Drop all border styles before continue, // so there are no leftovers which may conflict with // new border styles. // for ( var key in tmpStyles ) { // if ( key.indexOf( 'border' ) == 0 ) { // delete tmpStyles[ key ]; // } // } element.attributes.style = CKEDITOR.tools.writeCssText( tmpStyles ); // Unify background color property. if ( styles.background ) { var bg = CKEDITOR.tools.style.parse.background( styles.background ); if ( bg.color ) { Style.setStyle( element, 'background-color', bg.color, true ); Style.setStyle( element, 'background', '' ); } } // Unify border properties. for ( var border in borders ) { var borderStyle = styles[ border ] ? CKEDITOR.tools.style.border.fromCssRule( styles[ border ] ) : borders[ border ]; // No need for redundant shorthand properties if style is disabled. if ( borderStyle.style === 'none' ) { Style.setStyle( element, border, 'none' ); } else { Style.setStyle( element, border, borderStyle.toString() ); } } Style.mapCommonStyles( element ); Style.convertStyleToPx( element ); Style.createStyleStack( element, filter, editor, /margin|text\-align|padding|list\-style\-type|width|height|border|white\-space|vertical\-align|background/i ); }, 'font': function( element ) { if ( element.attributes.face && availableFonts ) { element.attributes.face = replaceWithMatchingFont( element.attributes.face, availableFonts ); } } } }; }; /** * Namespace containing all the helper functions to work with styles. * * @private * @since 4.13.0 * @member CKEDITOR.plugins.pastetools.filters.common */ plug.styles = { setStyle: function( element, key, value, dontOverwrite ) { var styles = tools.parseCssText( element.attributes.style ); if ( dontOverwrite && styles[ key ] ) { return; } if ( value === '' ) { delete styles[ key ]; } else { styles[ key ] = value; } element.attributes.style = CKEDITOR.tools.writeCssText( styles ); }, convertStyleToPx: function( element ) { var style = element.attributes.style; if ( !style ) { return; } element.attributes.style = style.replace( /\d+(\.\d+)?pt/g, function( match ) { return CKEDITOR.tools.convertToPx( match ) + 'px'; } ); }, // Map attributes to styles. mapStyles: function( element, attributeStyleMap ) { for ( var attribute in attributeStyleMap ) { if ( element.attributes[ attribute ] ) { if ( typeof attributeStyleMap[ attribute ] === 'function' ) { attributeStyleMap[ attribute ]( element.attributes[ attribute ] ); } else { Style.setStyle( element, attributeStyleMap[ attribute ], element.attributes[ attribute ] ); } delete element.attributes[ attribute ]; } } }, // Maps common attributes to styles. mapCommonStyles: function( element ) { return Style.mapStyles( element, { vAlign: function( value ) { Style.setStyle( element, 'vertical-align', value ); }, width: function( value ) { Style.setStyle( element, 'width', fixValue( value ) ); }, height: function( value ) { Style.setStyle( element, 'height', fixValue( value ) ); } } ); }, /** * Filters Word-specific styles for a given element. It may also filter additional styles * based on the `editor` configuration. * * @private * @since 4.13.0 * @param {CKEDITOR.htmlParser.element} element * @param {CKEDITOR.editor} editor * @member CKEDITOR.plugins.pastetools.filters.common.styles */ normalizedStyles: function( element, editor ) { // Some styles and style values are redundant, so delete them. var resetStyles = [ 'background-color:transparent', 'border-image:none', 'color:windowtext', 'direction:ltr', 'mso-', 'visibility:visible', 'div:border:none' // This one stays because https://dev.ckeditor.com/ticket/6241 ], textStyles = [ 'font-family', 'font', 'font-size', 'color', 'background-color', 'line-height', 'text-decoration' ], matchStyle = function() { var keys = []; for ( var i = 0; i < arguments.length; i++ ) { if ( arguments[ i ] ) { keys.push( arguments[ i ] ); } } return tools.indexOf( resetStyles, keys.join( ':' ) ) !== -1; }, removeFontStyles = CKEDITOR.plugins.pastetools.getConfigValue( editor, 'removeFontStyles' ) === true; var styles = tools.parseCssText( element.attributes.style ); if ( element.name == 'cke:li' ) { // IE8 tries to emulate list indentation with a combination of // text-indent and left margin. Normalize this. Note that IE8 styles are uppercase. if ( styles[ 'TEXT-INDENT' ] && styles.MARGIN ) { element.attributes[ 'cke-indentation' ] = plug.lists.getElementIndentation( element ); styles.MARGIN = styles.MARGIN.replace( /(([\w\.]+ ){3,3})[\d\.]+(\w+$)/, '$10$3' ); } else { // Remove text indent in other cases, because it works differently with lists in html than in Word. delete styles[ 'TEXT-INDENT' ]; } delete styles[ 'text-indent' ]; } var keys = this.keys( styles ); for ( var i = 0; i < keys.length; i++ ) { var styleName = keys[ i ].toLowerCase(), styleValue = styles[ keys[ i ] ], indexOf = CKEDITOR.tools.indexOf, toBeRemoved = removeFontStyles && indexOf( textStyles, styleName.toLowerCase() ) !== -1; if ( toBeRemoved || matchStyle( null, styleName, styleValue ) || matchStyle( null, styleName.replace( /\-.*$/, '-' ) ) || matchStyle( null, styleName ) || matchStyle( element.name, styleName, styleValue ) || matchStyle( element.name, styleName.replace( /\-.*$/, '-' ) ) || matchStyle( element.name, styleName ) || matchStyle( styleValue ) ) { delete styles[ keys[ i ] ]; } } var keepZeroMargins = CKEDITOR.plugins.pastetools.getConfigValue( editor, 'keepZeroMargins' ); // Still some elements might have shorthand margins or longhand with zero values. parseShorthandMargins( styles ); normalizeMargins(); return CKEDITOR.tools.writeCssText( styles ); function normalizeMargins() { var keys = [ 'top', 'right', 'bottom', 'left' ]; CKEDITOR.tools.array.forEach( keys, function( key ) { key = 'margin-' + key; if ( !( key in styles ) ) { return; } var value = CKEDITOR.tools.convertToPx( styles[ key ] ); // We need to get rid of margins, unless they are allowed in config (#2935). if ( value || keepZeroMargins ) { styles[ key ] = value ? value + 'px' : 0; } else { delete styles[ key ]; } } ); } }, /** * Surrounds the element's children with a stack of `<span>` elements, each one having one style * originally belonging to the element. * * @private * @since 4.13.0 * @param {CKEDITOR.htmlParser.element} element * @param {CKEDITOR.htmlParser.filter} filter * @param {CKEDITOR.editor} editor * @param {RegExp} [skipStyles] All matching style names will not be extracted to the style stack. Defaults * to `/margin((?!-)|-left|-top|-bottom|-right)|text-indent|text-align|width|border|padding/i`. * @member CKEDITOR.plugins.pastetools.filters.common.styles */ createStyleStack: function( element, filter, editor, skipStyles ) { var children = [], i; element.filterChildren( filter ); // Store element's children somewhere else. for ( i = element.children.length - 1; i >= 0; i-- ) { children.unshift( element.children[ i ] ); element.children[ i ].remove(); } Style.sortStyles( element ); // Create a stack of spans with each containing one style. var styles = tools.parseCssText( Style.normalizedStyles( element, editor ) ), innermostElement = element, styleTopmost = element.name === 'span'; // Ensure that the root element retains at least one style. for ( var style in styles ) { if ( style.match( skipStyles || /margin((?!-)|-left|-top|-bottom|-right)|text-indent|text-align|width|border|padding/i ) ) { continue; } if ( styleTopmost ) { styleTopmost = false; continue; } var newElement = new CKEDITOR.htmlParser.element( 'span' ); newElement.attributes.style = style + ':' + styles[ style ]; innermostElement.add( newElement ); innermostElement = newElement; delete styles[ style ]; } if ( !CKEDITOR.tools.isEmpty( styles ) ) { element.attributes.style = CKEDITOR.tools.writeCssText( styles ); } else { delete element.attributes.style; } // Add the stored children to the innermost span. for ( i = 0; i < children.length; i++ ) { innermostElement.add( children[ i ] ); } }, // Some styles need to be stacked in a particular order to work properly. sortStyles: function( element ) { var orderedStyles = [ 'border', 'border-bottom', 'font-size', 'background' ], style = tools.parseCssText( element.attributes.style ), keys = this.keys( style ), sortedKeys = [], nonSortedKeys = []; // Divide styles into sorted and non-sorted, because Array.prototype.sort() // requires a transitive relation. for ( var i = 0; i < keys.length; i++ ) { if ( tools.indexOf( orderedStyles, keys[ i ].toLowerCase() ) !== -1 ) { sortedKeys.push( keys[ i ] ); } else { nonSortedKeys.push( keys[ i ] ); } } // For styles in orderedStyles[] enforce the same order as in orderedStyles[]. sortedKeys.sort( function( a, b ) { var aIndex = tools.indexOf( orderedStyles, a.toLowerCase() ); var bIndex = tools.indexOf( orderedStyles, b.toLowerCase() ); return aIndex - bIndex; } ); keys = [].concat( sortedKeys, nonSortedKeys ); var sortedStyles = {}; for ( i = 0; i < keys.length; i++ ) { sortedStyles[ keys[ i ] ] = style[ keys[ i ] ]; } element.attributes.style = CKEDITOR.tools.writeCssText( sortedStyles ); }, keys: function(obj) { var hasOwnProperty = Object.prototype.hasOwnProperty, keys = [], dontEnums = CKEDITOR.tools.object.DONT_ENUMS, isNotObject = !obj || typeof obj !== 'object'; // We must handle non-object types differently in IE 8, // due to the fact that it uses ES5 behaviour, not ES2015+ as other browsers (#3381). if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && isNotObject ) { return createNonObjectKeys( obj ); } for ( var prop in obj ) { keys.push( prop ); } // Fix don't enum bug for IE < 9 browsers (#3120). if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) { for ( var i = 0; i < dontEnums.length; i++ ) { if ( hasOwnProperty.call( obj, dontEnums[ i ] ) ) { keys.push( dontEnums[ i ] ); } } } return keys; function createNonObjectKeys( value ) { var keys = [], i; if ( typeof value !== 'string' ) { return keys; } for ( i = 0; i < value.length; i++ ) { keys.push( String( i ) ); } return keys; } }, /** * Moves the element styles lower in the DOM hierarchy. If `wrapText==true` and the direct child of an element * is a text node, it will be wrapped in a `<span>` element. * * @private * @since 4.13.0 * @param {CKEDITOR.htmlParser.element} element * @param {Object} exceptions An object containing style names which should not be moved, e.g. `{ background: true }`. * @param {Boolean} [wrapText=false] Whether a direct text child of an element should be wrapped into a `<span>` tag * so that the styles can be moved to it. * @returns {Boolean} Returns `true` if the styles were successfully moved lower. * @member CKEDITOR.plugins.pastetools.filters.common.styles */ pushStylesLower: function( element, exceptions, wrapText ) { if ( !element.attributes.style || element.children.length === 0 ) { return false; } exceptions = exceptions || {}; // Entries ending with a dash match styles that start with // the entry name, e.g. 'border-' matches 'border-style', 'border-color' etc. var retainedStyles = { 'list-style-type': true, 'width': true, 'height': true, 'border': true, 'border-': true }; var styles = tools.parseCssText( element.attributes.style ); for ( var style in styles ) { if ( style.toLowerCase() in retainedStyles || retainedStyles [ style.toLowerCase().replace( /\-.*$/, '-' ) ] || style.toLowerCase() in exceptions ) { continue; } var pushed = false; for ( var i = 0; i < element.children.length; i++ ) { var child = element.children[ i ]; if ( child.type === CKEDITOR.NODE_TEXT && wrapText ) { var wrapper = new CKEDITOR.htmlParser.element( 'span' ); wrapper.setHtml( child.value ); child.replaceWith( wrapper ); child = wrapper; } if ( child.type !== CKEDITOR.NODE_ELEMENT ) { continue; } pushed = true; Style.setStyle( child, style, styles[ style ] ); } if ( pushed ) { delete styles[ style ]; } } element.attributes.style = CKEDITOR.tools.writeCssText( styles ); return true; }, /** * Namespace containing the styles inliner. * * @since 4.13.0 * @private * @member CKEDITOR.plugins.pastetools.filters.common.styles */ inliner: { /** * * Styles skipped by the styles inliner. * * @property {String[]} * @private * @since 4.13.0 * @member CKEDITOR.plugins.pastetools.filters.common.styles.inliner */ filtered: [ 'break-before', 'break-after', 'break-inside', 'page-break', 'page-break-before', 'page-break-after', 'page-break-inside' ], /** * Parses the content of the provided `<style>` element. * * @param {CKEDITOR.dom.element/String} styles The `<style>` element or CSS text. * @returns {Array} An array containing parsed styles. Each item (style) is an object containing two properties: * * selector &ndash; A string representing a CSS selector. * * styles &ndash; An object containing a list of styles (e.g. `{ margin: 0, text-align: 'left' }`). * @since 4.13.0 * @private * @member CKEDITOR.plugins.pastetools.filters.common.styles.inliner */ parse: function( styles ) { var parseCssText = CKEDITOR.tools.parseCssText, filterStyles = Style.inliner.filter, sheet = styles.is ? styles.$.sheet : createIsolatedStylesheet( styles ); function createIsolatedStylesheet( styles ) { var style = new CKEDITOR.dom.element( 'style' ), iframe = new CKEDITOR.dom.element( 'iframe' ); iframe.hide(); CKEDITOR.document.getBody().append( iframe ); iframe.$.contentDocument.documentElement.appendChild( style.$ ); style.$.textContent = styles; iframe.remove(); return style.$.sheet; } function getStyles( cssText ) { var startIndex = cssText.indexOf( '{' ), endIndex = cssText.indexOf( '}' ); return parseCssText( cssText.substring( startIndex + 1, endIndex ), true ); } var parsedStyles = [], rules, i; if ( sheet ) { rules = sheet.cssRules; for ( i = 0; i < rules.length; i++ ) { // To detect if the rule contains styles and is not an at-rule, it's enough to check rule's type. if ( rules[ i ].type === window.CSSRule.STYLE_RULE ) { parsedStyles.push( { selector: rules[ i ].selectorText, styles: filterStyles( getStyles( rules[ i ].cssText ) ) } ); } } } return parsedStyles; }, /** * Filters out all unnecessary styles. * * @param {Object} stylesObj An object containing parsed CSS declarations * as property/value pairs (see {@link CKEDITOR.plugins.pastetools.filters.common.styles.inliner#parse}). * @returns {Object} The `stylesObj` copy with specific styles filtered out. * @since 4.13.0 * @private * @member CKEDITOR.plugins.pastetools.filters.common.styles.inliner */ filter: function( stylesObj ) { var toRemove = Style.inliner.filtered, indexOf = tools.array.indexOf, newObj = {}, style; for ( style in stylesObj ) { if ( indexOf( toRemove, style ) === -1 ) { newObj[ style ] = stylesObj[ style ]; } } return newObj; }, /** * Sorts the given styles array. All rules containing class selectors will have lower indexes than the rest * of the rules. Selectors with the same priority will be sorted in a reverse order than in the input array. * * @param {Array} stylesArray An array of styles as returned from * {@link CKEDITOR.plugins.pastetools.filters.common.styles.inliner#parse}. * @returns {Array} Sorted `stylesArray`. * @since 4.13.0 * @private * @member CKEDITOR.plugins.pastetools.filters.common.styles.inliner */ sort: function( stylesArray ) { // Returns comparison function which sorts all selectors in a way that class selectors are ordered // before the rest of the selectors. The order of the selectors with the same specificity // is reversed so that the most important will be applied first. function getCompareFunction( styles ) { var order = CKEDITOR.tools.array.map( styles, function( item ) { return item.selector; } ); return function( style1, style2 ) { var value1 = isClassSelector( style1.selector ) ? 1 : 0, value2 = isClassSelector( style2.selector ) ? 1 : 0, result = value2 - value1; // If the selectors have same specificity, the latter one should // have higher priority (goes first). return result !== 0 ? result : order.indexOf( style2.selector ) - order.indexOf( style1.selector ); }; } // True if given CSS selector contains a class selector. function isClassSelector( selector ) { return ( '' + selector ).indexOf( '.' ) !== -1; } return stylesArray.sort( getCompareFunction( stylesArray ) ); }, /** * Finds and inlines all the `<style>` elements in a given `html` string and returns a document where * all the styles are inlined into appropriate elements. * * This is needed because sometimes Microsoft Word does not put the style directly into the element, but * into a generic style sheet. * * @param {String} html An HTML string to be parsed. * @returns {CKEDITOR.dom.document} * @since 4.13.0 * @private * @member CKEDITOR.plugins.pastetools.filters.common.styles.inliner */ inline: function( html ) { var parseStyles = Style.inliner.parse, sortStyles = Style.inliner.sort, document = createTempDocument( html ), stylesTags = document.find( 'style' ), stylesArray = sortStyles( parseStyleTags( stylesTags ) ); function createTempDocument( html ) { var parser = new DOMParser(), document = parser.parseFromString( html, 'text/html' ); return new CKEDITOR.dom.document( document ); } function parseStyleTags( stylesTags ) { var styles = [], i; for ( i = 0; i < stylesTags.count(); i++ ) { styles = styles.concat( parseStyles( stylesTags.getItem( i ) ) ); } return styles; } function applyStyle( document, selector, style ) { var elements = document.find( selector ), element, oldStyle, newStyle, i; parseShorthandMargins( style ); for ( i = 0; i < elements.count(); i++ ) { element = elements.getItem( i ); oldStyle = CKEDITOR.tools.parseCssText( element.getAttribute( 'style' ) ); parseShorthandMargins( oldStyle ); // The styles are applied with decreasing priority so we do not want // to overwrite the existing properties. newStyle = CKEDITOR.tools.extend( {}, oldStyle, style ); element.setAttribute( 'style', CKEDITOR.tools.writeCssText( newStyle ) ); } } CKEDITOR.tools.array.forEach( stylesArray, function( style ) { applyStyle( document, style.selector, style.styles ); } ); return document; } } }; Style = plug.styles; plug.lists = { getElementIndentation: function( element ) { var style = tools.parseCssText( element.attributes.style ); if ( style.margin || style.MARGIN ) { style.margin = style.margin || style.MARGIN; var fakeElement = { styles: { margin: style.margin } }; CKEDITOR.filter.transformationsTools.splitMarginShorthand( fakeElement ); style[ 'margin-left' ] = fakeElement.styles[ 'margin-left' ]; } return parseInt( tools.convertToPx( style[ 'margin-left' ] || '0px' ), 10 ); } }; /** * Namespace containing all the helper functions to work with elements. * * @private * @since 4.13.0 * @member CKEDITOR.plugins.pastetools.filters.common */ plug.elements = { /** * Replaces an element with its children. * * This function is customized to work inside filters. * * @private * @since 4.13.0 * @param {CKEDITOR.htmlParser.element} element * @member CKEDITOR.plugins.pastetools.filters.common.elements */ replaceWithChildren: function( element ) { for ( var i = element.children.length - 1; i >= 0; i-- ) { element.children[ i ].insertAfter( element ); } } }; plug.createAttributeStack = createAttributeStack; plug.parseShorthandMargins = parseShorthandMargins; /** * Namespace containing all the helper functions to work with [RTF](https://interoperability.blob.core.windows.net/files/Archive_References/%5bMSFT-RTF%5d.pdf). * * @private * @since 4.16.0 * @member CKEDITOR.plugins.pastetools.filters.common */ plug.rtf = { /** * Get all groups from the RTF content with the given name. * * ```js * var rtfContent = '{\\rtf1\\some\\control\\words{\\group content}{\\group content}{\\whatever {\\subgroup content}}}', * groups = CKEDITOR.plugins.pastetools.filters.common.rtf.getGroups( rtfContent, '(group|whatever)' ); * * console.log( groups ); * * // Result of the console.log: * // [ * // {"start":25,"end":41,"content":"{\\group content}"}, * // {"start":41,"end":57,"content":"{\\group content}"}, * // {"start":57,"end":88,"content":"{\\whatever {\\subgroup content}}"} * // ] * ``` * * @private * @since 4.16.0 * @param {String} rtfContent * @param {String} groupName Group name to find. It can be a regex-like string. * @returns {CKEDITOR.plugins.pastetools.filters.common.rtf.GroupInfo[]} * @member CKEDITOR.plugins.pastetools.filters.common.rtf */ getGroups: function( rtfContent, groupName ) { var groups = [], current, from = 0; while ( current = plug.rtf.getGroup( rtfContent, groupName, { start: from } ) ) { from = current.end; groups.push( current ); } return groups; }, /** * Remove all groups from the RTF content with the given name. * * ```js * var rtfContent = '{\\rtf1\\some\\control\\words{\\group content}{\\group content}{\\whatever {\\subgroup content}}}', * rtfWithoutGroups = CKEDITOR.plugins.pastetools.filters.common.rtf.removeGroups( rtfContent, '(group|whatever)' ); * * console.log( rtfWithoutGroups ); // {\rtf1\some\control\words} * ``` * * @private * @since 4.16.0 * @param {String} rtfContent * @param {String} groupName Group name to find. It can be a regex-like string. * @returns {String} RTF content without the removed groups. * @member CKEDITOR.plugins.pastetools.filters.common.rtf */ removeGroups: function( rtfContent, groupName ) { var current; while ( current = plug.rtf.getGroup( rtfContent, groupName ) ) { var beforeContent = rtfContent.substring( 0, current.start ), afterContent = rtfContent.substring( current.end ); rtfContent = beforeContent + afterContent; } return rtfContent; }, /** * Get the group from the RTF content with the given name. * * Groups are recognized thanks to being in `{\<name>}` format. * * ```js * var rtfContent = '{\\rtf1\\some\\control\\words{\\group content1}{\\group content2}{\\whatever {\\subgroup content}}}', * firstGroup = CKEDITOR.plugins.pastetools.filters.common.rtf.getGroup( rtfContent, '(group|whatever)' ), * lastGroup = CKEDITOR.plugins.pastetools.filters.common.rtf.getGroup( rtfContent, '(group|whatever)', { * start: 50 * } ); * * console.log( firstGroup ); // {"start":25,"end":42,"content":"{\\group content1}"} * console.log( lastGroup ); // {"start":59,"end":90,"content":"{\\whatever {\\subgroup content}}"} * ``` * * @private * @since 4.16.0 * @param {String} content RTF content. * @param {String} groupName Group name to find. It can be a regex-like string. * @param {Object} options Additional options. * @param {Number} options.start String index on which the search should begin. * @returns {CKEDITOR.plugins.pastetools.filters.common.rtf.GroupInfo} * @member CKEDITOR.plugins.pastetools.filters.common.rtf */ getGroup: function( content, groupName, options ) { // This function is in fact a very primitive RTF parser. // It iterates over RTF content and search for the last } in the group // by keeping track of how many elements are open using a stack-like method. var open = 0, // Despite the fact that we search for only one group, // the global modifier is used to be able to manipulate // the starting index of the search. Without g flag it's impossible. startRegex = new RegExp( '\\{\\\\' + groupName, 'g' ), group, i, current; options = this.merge( { start: 0 }, options || {} ); startRegex.lastIndex = options.start; group = startRegex.exec( content ); if ( !group ) { return null; } i = group.index; current = content[ i ]; do { // Every group start has format of {\. However there can be some whitespace after { and before /. // Additionally we need to filter also curly braces from the content – fortunately they are escaped. var isValidGroupStart = current === '{' && getPreviousNonWhitespaceChar( content, i ) !== '\\' && getNextNonWhitespaceChar( content, i ) === '\\', isValidGroupEnd = current === '}' && getPreviousNonWhitespaceChar( content, i ) !== '\\' && open > 0; if ( isValidGroupStart ) { open++; } else if ( isValidGroupEnd ) { open--; } current = content[ ++i ]; } while ( current && open > 0 ); return { start: group.index, end: i, content: content.substring( group.index, i ) }; }, merge: function( obj1, obj2 ) { var tools = CKEDITOR.tools, self = this, copy1 = tools.clone( obj1 ), copy2 = tools.clone( obj2 ); self.forEach( self.keys( copy2 ), function( key ) { if ( typeof copy2[ key ] === 'object' && typeof copy1[ key ] === 'object' ) { copy1[ key ] = self.merge( copy1[ key ], copy2[ key ] ); } else { copy1[ key ] = copy2[ key ]; } } ); return copy1; }, keys: function( obj ) { var hasOwnProperty = Object.prototype.hasOwnProperty, keys = [], dontEnums = CKEDITOR.tools.object.DONT_ENUMS, isNotObject = !obj || typeof obj !== 'object'; // We must handle non-object types differently in IE 8, // due to the fact that it uses ES5 behaviour, not ES2015+ as other browsers (#3381). if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && isNotObject ) { return createNonObjectKeys( obj ); } for ( var prop in obj ) { keys.push( prop ); } // Fix don't enum bug for IE < 9 browsers (#3120). if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) { for ( var i = 0; i < dontEnums.length; i++ ) { if ( hasOwnProperty.call( obj, dontEnums[ i ] ) ) { keys.push( dontEnums[ i ] ); } } } return keys; function createNonObjectKeys( value ) { var keys = [], i; if ( typeof value !== 'string' ) { return keys; } for ( i = 0; i < value.length; i++ ) { keys.push( String( i ) ); } return keys; } }, forEach: function( array, fn, thisArg ) { var len = array.length, i; for ( i = 0; i < len; i++ ) { fn.call( thisArg, array[ i ], i, array ); } }, /** * Get group content. * * The content starts with the first character that is not a part of * control word or subgroup. * * ```js * var group = '{\\group{\\subgroup subgroupcontent} group content}', * groupContent = CKEDITOR.plugins.pastetools.filters.common.rtf.extractGroupContent( group ); * * console.log( groupContent ); // "group content" * ``` * * @private * @since 4.16.0 * @param {String} group Whole group string. * @returns {String} Extracted group content. * @member CKEDITOR.plugins.pastetools.filters.common.rtf */ extractGroupContent: function( group ) { var groupName = getGroupName( group ), controlWordsRegex = /^\{(\\[\w-]+\s*)+/g, // Sometimes content follows the last subgroup without any space. // We need to add it to correctly parse the whole thing. subgroupWithousSpaceRegex = /\}([^{\s]+)/g; group = group.replace( subgroupWithousSpaceRegex, '} $1' ); // And now remove all subgroups that are not the actual group. group = plug.rtf.removeGroups( group, '(?!' + groupName + ')' ); // Remove all control words and trim the whitespace at the beginning // that could be introduced by preserving space after last subgroup. group = CKEDITOR.tools.trim( group.replace( controlWordsRegex, '' ) ); // What's left is group content with } at the end. return group.replace( /}$/, '' ); } }; function getGroupName( group ) { var groupNameRegex = /^\{\\(\w+)/, groupName = group.match( groupNameRegex ); if ( !groupName ) { return null; } return groupName[ 1 ]; } function getPreviousNonWhitespaceChar( content, index ) { return getNonWhitespaceChar( content, index, -1 ); } function getNextNonWhitespaceChar( content, index ) { return getNonWhitespaceChar( content, index, 1 ); } function getNonWhitespaceChar( content, startIndex, direction ) { var index = startIndex + direction, current = content[ index ], whiteSpaceRegex = /[\s]/; while ( current && whiteSpaceRegex.test( current ) ) { index = index + direction; current = content[ index ]; } return current; } function fixValue( value ) { // Add 'px' only for values which are not ended with % var endsWithPercent = /%$/; return endsWithPercent.test( value ) ? value : value + 'px'; } // Same as createStyleStack, but instead of styles - stack attributes. function createAttributeStack( element, filter ) { var i, children = []; element.filterChildren( filter ); // Store element's children somewhere else. for ( i = element.children.length - 1; i >= 0; i-- ) { children.unshift( element.children[ i ] ); element.children[ i ].remove(); } // Create a stack of spans with each containing one style. var attributes = element.attributes, innermostElement = element, topmost = true; for ( var attribute in attributes ) { if ( topmost ) { topmost = false; continue; } var newElement = new CKEDITOR.htmlParser.element( element.name ); newElement.attributes[ attribute ] = attributes[ attribute ]; innermostElement.add( newElement ); innermostElement = newElement; delete attributes[ attribute ]; } // Add the stored children to the innermost span. for ( i = 0; i < children.length; i++ ) { innermostElement.add( children[ i ] ); } } function parseShorthandMargins( style ) { var marginCase = style.margin ? 'margin' : style.MARGIN ? 'MARGIN' : false, key, margin; if ( marginCase ) { margin = CKEDITOR.tools.style.parse.margin( style[ marginCase ] ); for ( key in margin ) { style[ 'margin-' + key ] = margin[ key ]; } delete style[ marginCase ]; } } function removeSuperfluousStyles( element ) { var resetStyles = [ 'background-color:transparent', 'background:transparent', 'background-color:none', 'background:none', 'background-position:initial initial', 'background-repeat:initial initial', 'caret-color', 'font-family:-webkit-standard', 'font-variant-caps', 'letter-spacing:normal', 'orphans', 'widows', 'text-transform:none', 'word-spacing:0px', '-webkit-text-size-adjust:auto', '-webkit-text-stroke-width:0px', 'text-indent:0px', 'margin-bottom:0in' ]; var styles = CKEDITOR.tools.parseCssText( element.attributes.style ), styleName, styleString; for ( styleName in styles ) { styleString = styleName + ':' + styles[ styleName ]; if ( some( resetStyles, function( val ) { return styleString.substring( 0, val.length ).toLowerCase() === val; } ) ) { delete styles[ styleName ]; continue; } } styles = CKEDITOR.tools.writeCssText( styles ); if ( styles !== '' ) { element.attributes.style = styles; } else { delete element.attributes.style; } } function getMatchingFonts( editor ) { var fontNames = editor.config.font_names, validNames = []; if ( !fontNames || !fontNames.length ) { return false; } validNames = CKEDITOR.tools.array.map( fontNames.split( ';' ), function( value ) { // Font can have a short name at the begining. It's necessary to remove it, to apply correct style. if ( value.indexOf( '/' ) === -1 ) { return value; } return value.split( '/' )[ 1 ]; } ); return validNames.length ? validNames : false; } function some(array, fn, thisArg) { for ( var i = 0; i < array.length; i++ ) { if ( fn.call( thisArg, array[ i ], i, array ) ) { return true; } } return false; } function keys(obj) { var hasOwnProperty = Object.prototype.hasOwnProperty, keys = [], dontEnums = CKEDITOR.tools.object.DONT_ENUMS, isNotObject = !obj || typeof obj !== 'object'; // We must handle non-object types differently in IE 8, // due to the fact that it uses ES5 behaviour, not ES2015+ as other browsers (#3381). if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && isNotObject ) { return createNonObjectKeys( obj ); } for ( var prop in obj ) { keys.push( prop ); } // Fix don't enum bug for IE < 9 browsers (#3120). if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) { for ( var i = 0; i < dontEnums.length; i++ ) { if ( hasOwnProperty.call( obj, dontEnums[ i ] ) ) { keys.push( dontEnums[ i ] ); } } } return keys; function createNonObjectKeys( value ) { var keys = [], i; if ( typeof value !== 'string' ) { return keys; } for ( i = 0; i < value.length; i++ ) { keys.push( String( i ) ); } return keys; } } function replaceWithMatchingFont( fontValue, availableFonts ) { var fontParts = fontValue.split( ',' ), matchingFont = find( availableFonts, function( font ) { for ( var i = 0; i < fontParts.length; i++ ) { if ( font.indexOf( trim( fontParts[ i ] ) ) === -1 ) { return false; } } return true; } ); return matchingFont || fontValue; } function find(array, fn, thisArg) { var length = array.length, i = 0; while ( i < length ) { if ( fn.call( thisArg, array[ i ], i, array ) ) { return array[ i ]; } i++; } return undefined; } function trim() { // We are not using \s because we don't want "non-breaking spaces" to be caught. var trimRegex = /(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g; return function( str ) { return str.replace( trimRegex, '' ); }; } function normalizeAttributesName( element ) { if ( element.attributes.bgcolor ) { var styles = CKEDITOR.tools.parseCssText( element.attributes.style ); if ( !styles[ 'background-color' ] ) { styles[ 'background-color' ] = element.attributes.bgcolor; element.attributes.style = CKEDITOR.tools.writeCssText( styles ); } } } /** * Virtual class that illustrates group info * returned by {@link CKEDITOR.plugins.pastetools.filters.common.rtf#getGroup} method. * * @since 4.16.0 * @class CKEDITOR.plugins.pastetools.filters.common.rtf.GroupInfo * @abstract */ /** * String index, on which the group starts. * * @property {Number} start * @member CKEDITOR.plugins.pastetools.filters.common.rtf.GroupInfo */ /** * String index, on which the group ends. * * @property {Number} end * @member CKEDITOR.plugins.pastetools.filters.common.rtf.GroupInfo */ /** * The whole group, including control words and subgroups. * * @property {String} content * @member CKEDITOR.plugins.pastetools.filters.common.rtf.GroupInfo */ /** * Whether to ignore all font-related formatting styles, including: * * * font size, * * font family, * * font foreground and background color. * * ```js * config.pasteTools_removeFontStyles = true; * ``` * * **Important note:** This configuration option is deprecated. * Either configure a proper {@glink guide/dev_advanced_content_filter Advanced Content Filter} for the editor * or use the {@link CKEDITOR.editor#afterPasteFromWord} event. * * @deprecated 4.13.0 * @since 4.13.0 * @cfg {Boolean} [pasteTools_removeFontStyles=false] * @member CKEDITOR.config */ /** * Whether the `margin` style of a pasted element that equals to 0 should be removed. * * ```js * // Disable removing `margin:0`, `margin-left:0`, etc. * config.pasteTools_keepZeroMargins = true; * ``` * * @since 4.13.0 * @cfg {Boolean} [pasteTools_keepZeroMargins=false] * @member CKEDITOR.config */ } )();