Skip to content

Commit

Permalink
Introduce paste from libre office plugin (#3624)
Browse files Browse the repository at this point in the history
Introduce paste from libre office plugin
  • Loading branch information
Comandeer authored Jan 20, 2020
2 parents 8794f9f + 034aa54 commit 294ea33
Show file tree
Hide file tree
Showing 194 changed files with 104,734 additions and 513 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

New features:

* [#2374](https://github.com/ckeditor/ckeditor-dev/issues/2374): Added support for pasting rich content from LibreOffice with the [Paste from LibreOffice](https://ckeditor.com/cke4/addon/pastefromlibreoffice) plugin.
* [#2583](https://github.com/ckeditor/ckeditor-dev/issues/2583): Changed [Emoji](https://ckeditor.com/cke4/addon/emoji) suggestion box to show matched emoji name instead of an ID.
* [#3748](https://github.com/ckeditor/ckeditor-dev/issues/3748): Improved [Color Button](https://ckeditor.com/cke4/addon/colorbutton) state to reflect selected editor content colors.

Expand Down
1 change: 1 addition & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ CKEDITOR.editorConfig = function( config ) {
'newpage,' +
'pagebreak,' +
'pastefromgdocs,' +
'pastefromlibreoffice,' +
'pastefromword,' +
'pastetext,' +
'preview,' +
Expand Down
1 change: 1 addition & 0 deletions dev/builder/build-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ var CKBUILDER_CONFIG = {
maximize: 1,
newpage: 1,
pagebreak: 1,
pastefromlibreoffice: 1,
pastefromword: 1,
pastetext: 1,
preview: 1,
Expand Down
334 changes: 334 additions & 0 deletions plugins/pastefromlibreoffice/filter/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/* globals CKEDITOR */

( function() {
'use strict';

var pastetools = CKEDITOR.plugins.pastetools,
commonFilter = pastetools.filters.common,
Style = commonFilter.styles;

/**
* Set of Paste from Libre Office plugin helpers.
*
* @since 4.14.0
* @private
* @member CKEDITOR.plugins.pastetools.filters
*/
CKEDITOR.plugins.pastetools.filters.libreoffice = {
/**
* Rules for the Paste from Libre Office filter.
*
* @since 4.14.0
* @private
* @member CKEDITOR.plugins.pastetools.filters.libreoffice
*/
rules: function( html, editor, filter ) {
return {
root: function( element ) {
element.filterChildren( filter );
},

comment: function() {
return false;
},

elementNames: [
[ /^head$/i, '' ],
[ /^meta$/i, '' ],
[ /^strike$/i, 's' ]
],

elements: {
// Required due to bug (#3664).
'!doctype': function( el ) {
el.replaceWithChildren();
},

'span': function( element ) {
if ( element.attributes.style ) {
element.attributes.style = Style.normalizedStyles( element, editor );

Style.createStyleStack( element, filter, editor );
}

replaceEmptyElementWithChildren( element );
},

'p': function( element ) {
var styles = CKEDITOR.tools.parseCssText( element.attributes.style );

if ( editor.plugins.pagebreak &&
( styles[ 'page-break-before' ] === 'always' || styles[ 'break-before' ] === 'page' )
) {
insertPageBreakBefore( editor, element );
}

if ( element.attributes.align ) {
styles[ 'text-align' ] = element.attributes.align;
delete element.attributes.align;
}

element.attributes.style = CKEDITOR.tools.writeCssText( styles );

element.filterChildren( filter );
Style.createStyleStack( element, filter, editor );
},

'div': function( element ) {
Style.createStyleStack( element, filter, editor );
},

'a': function( el ) {
if ( el.attributes.style ) {
el.attributes.style = removeDefaultLinkStyles( el.attributes.style );
}
},

'h1': function( el ) {
Style.createStyleStack( el, filter, editor );
},

'h2': function( el ) {
Style.createStyleStack( el, filter, editor );
},

'h3': function( el ) {
Style.createStyleStack( el, filter, editor );
},

'h4': function( el ) {
Style.createStyleStack( el, filter, editor );
},

'h5': function( el ) {
Style.createStyleStack( el, filter, editor );
},

'h6': function( el ) {
Style.createStyleStack( el, filter, editor );
},

'pre': function( el ) {
Style.createStyleStack( el, filter, editor );
},

'font': function( el ) {
if ( shouldReplaceFontWithChildren( el ) ) {
el.replaceWithChildren();
}

// 1. Case there is no style stack
// 2. font-size is child of this node
// 3. font-size is parent of this node
var styles = CKEDITOR.tools.parseCssText( el.attributes.style );
var firstChild = el.getFirst();

if ( el.attributes.size &&
firstChild &&
firstChild.type === CKEDITOR.NODE_ELEMENT &&
/font-size/.test( firstChild.attributes.style )
) {
el.replaceWithChildren();
}

if ( styles[ 'font-size' ] ) {
// We need to remove 'size' and transform font to span with 'font-size'.
delete el.attributes.size;
el.name = 'span';

if ( firstChild && firstChild.type === CKEDITOR.NODE_ELEMENT && firstChild.attributes.size ) {
firstChild.replaceWithChildren();
}
}
},

'ul': function( el ) {
if ( listMerger( el, filter ) ) {
return false;
}
},

'ol': function( el ) {
if ( listMerger( el, filter ) ) {
return false;
}
},

'img': function( el ) {
var src = el.attributes.src;

if ( !src ) {
return false;
}
}
},

attributes: {
'style': function( styles, element ) {
// Returning false deletes the attribute.
return Style.normalizedStyles( element, editor ) || false;
},

'cellspacing': remove,

'cellpadding': remove,

'border': remove
}
};
}
};

function replaceEmptyElementWithChildren( element ) {
if ( !CKEDITOR.tools.object.entries( element.attributes ).length ) {
element.replaceWithChildren();
}
}

function shouldReplaceFontWithChildren( element ) {
// Anchor is additionaly styled with font.
if ( element.parent.name === 'a' && element.attributes.color === '#000080' ) {
return true;
}

// Sub or sup is additionaly styled with font
if ( element.parent.children.length === 1 && ( element.parent.name === 'sup' || element.parent.name === 'sub' ) && element.attributes.size === '2' ) {
return true;
}

return false;
}

// Return true if sucesfuly merge list to previous item.
function listMerger( el, filter ) {
if ( !shouldMergeToPreviousList( el ) ) {
return false;
}

var previous = el.previous,
lastLi = getLastListItem( previous ),
liDepthValue = checkDepth( lastLi ),
innerList = unwrapList( el, liDepthValue );

if ( innerList ) {
lastLi.add( innerList );
innerList.filterChildren( filter );
return true;
}

return false;
}

function shouldMergeToPreviousList( element ) {
// There need to be previous list where element should be merged.
if ( !element.previous || !isList( element.previous ) ) {
return false;
}

// There might be cases in PFW where li element has no children (test Tickets/7131 word2013 chrome)
if ( !element.getFirst().children.length ) {
return false;
}

// Current list need to be nested list, what points that is sublist.
if ( element.children.length !== 1 || !isList( element.getFirst().getFirst() ) ) {
return false;
}

return true;
}

// Checks level of nested list for given element.
// It's exepected that argument is the `li` element.
function checkDepth( element ) {
var level = 0,
currentElement = element,
listEvaluator = getListEvaluator();

while ( ( currentElement = currentElement.getAscendant( listEvaluator ) ) ) {
level++;
}

return level;
}

function getLastListItem( el ) {
var lastChild = el.children[ el.children.length - 1 ];

if ( !isList( lastChild ) && lastChild.name !== 'li' ) {
return el;
}

return getLastListItem( lastChild );
}

function getListEvaluator() {
var isInBlock = false;

return function( element ) {
// There might be situation that list is somehow nested in other type of element, quotes, div, table, etc.
// When such situation happen we should not search for any above list.
if ( isInBlock ) {
return false;
}

if ( !isList( element ) && element.name !== 'li' ) {
isInBlock = true;
return false;
}

return isList( element );
};
}

// Get nested list by first items
function unwrapList( list, count ) {
if ( count ) {
return unwrapList( list.getFirst().getFirst(), --count );
}

return list;
}

function isList( element ) {
return element.name === 'ol' || element.name === 'ul';
}

function remove() {
return false;
}

function removeDefaultLinkStyles( styles ) {
var parsedStyles = CKEDITOR.tools.parseCssText( styles );

if ( parsedStyles.color === '#000080' ) {
delete parsedStyles.color;
}

if ( parsedStyles[ 'text-decoration' ] === 'underline' ) {
delete parsedStyles[ 'text-decoration' ];
}

return CKEDITOR.tools.writeCssText( parsedStyles );
}

function insertPageBreakBefore( editor, element ) {
var pagebreakEl = CKEDITOR.plugins.pagebreak.createElement( editor );

pagebreakEl = CKEDITOR.htmlParser.fragment.fromHtml( pagebreakEl.getOuterHtml() ).children[ 0 ];

pagebreakEl.insertBefore( element );
}

CKEDITOR.pasteFilters.libreoffice = pastetools.createFilter( {
rules: [
commonFilter.rules,
CKEDITOR.plugins.pastetools.filters.libreoffice.rules
]
} );
} )();
Loading

0 comments on commit 294ea33

Please sign in to comment.