diff --git a/.changelog/20260424115528_ck_18846.md b/.changelog/20260424115528_ck_18846.md new file mode 100644 index 00000000000..4340a1be7f2 --- /dev/null +++ b/.changelog/20260424115528_ck_18846.md @@ -0,0 +1,9 @@ +--- +type: Fix +scope: + - ckeditor5-paste-from-office +closes: + - https://github.com/ckeditor/ckeditor5/issues/18846 +--- + +Pasting content from Word no longer inserts unwanted visible bookmarks into the editor. diff --git a/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts b/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts index cc1a5ec88a4..7019da34d84 100644 --- a/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts +++ b/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts @@ -7,9 +7,10 @@ * @module paste-from-office/filters/bookmark */ -import { - type ViewUpcastWriter, - type ViewDocumentFragment +import type { + ViewUpcastWriter, + ViewDocumentFragment, + ViewElement } from '@ckeditor/ckeditor5-engine'; /** @@ -40,5 +41,26 @@ export function transformBookmarks( const children = element.getChildren(); writer.insertChild( index, children, element.parent! ); + + if ( isHiddenBookmarkAnchor( element ) ) { + writer.remove( element ); + } } } + +/** + * Checks whether the given element is a hidden or auto-generated bookmark anchor. + * + * Editors like MS Word and Google Docs use the `name` attribute (rather than `id`) + * for bookmarks. Furthermore, they reserve `_`-prefixed bookmark names for + * auto-generated anchors (e.g., Table of Contents or internal hyperlinks) and + * do not allow users to manually create custom bookmarks starting with an underscore. + * + * @param element The element to check. + * @returns True if the element is a hidden bookmark anchor, false otherwise. + */ +function isHiddenBookmarkAnchor( element: ViewElement ) { + const name = element.getAttribute( 'name' ); + + return !!name && name.startsWith( '_' ); +} diff --git a/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.safari.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.safari.word2016.html index 9841d0d70c3..1d1eca83cb5 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.safari.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.safari.word2016.html @@ -1,6 +1,6 @@
An example list with an error:
diff --git a/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.word2016.html index dfa2e32393e..8cba3323da1 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.word2016.html @@ -1,6 +1,6 @@An example list with an error:
diff --git a/packages/ckeditor5-paste-from-office/tests/filters/bookmark.js b/packages/ckeditor5-paste-from-office/tests/filters/bookmark.js index 9b403650c54..67d7d934c56 100644 --- a/packages/ckeditor5-paste-from-office/tests/filters/bookmark.js +++ b/packages/ckeditor5-paste-from-office/tests/filters/bookmark.js @@ -221,6 +221,46 @@ describe( 'PasteFromOffice - filters - bookmark', () => { ); } ); + it( 'should extract text and remove the element completely if its name starts with an underscore (hidden bookmark)', () => { + performTest( + '' + + 'text' + + '', + + 'text' + ); + } ); + + it( 'should completely remove an empty element if its name starts with an underscore', () => { + performTest( + 'paragraph
' + + '', + + 'paragraph
' + ); + } ); + + it( 'should remove the element but keep content when both id and name are present and name starts with an underscore', () => { + performTest( + '' + + 'text' + + '', + + 'text' + ); + } ); + + it( 'should NOT remove the element if only its id starts with an underscore (since Word/GDocs use name)', () => { + performTest( + '' + + 'text' + + '', + + '' + + 'text' + ); + } ); + function performTest( inputData, expectedData ) { const documentFragment = htmlDataProcessor.toView( inputData );